Skip to content

Commit

Permalink
Migrate to type-safe Compose navigation (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
mars885 authored Sep 26, 2024
1 parent 3883471 commit 36f607d
Show file tree
Hide file tree
Showing 40 changed files with 468 additions and 428 deletions.
211 changes: 114 additions & 97 deletions app/src/main/java/com/paulrybitskyi/gamedge/AppNavigation.kt

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions app/src/main/java/com/paulrybitskyi/gamedge/BottomBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,17 @@ private enum class BottomNavigationItemModel(
DISCOVER(
iconId = CoreR.drawable.compass_rose,
titleId = FeatureDiscoveryR.string.games_discovery_toolbar_title,
screen = Screen.Discover,
screen = Screen.GamesDiscovery,
),
LIKES(
iconId = CoreR.drawable.heart,
titleId = FeatureLikesR.string.liked_games_toolbar_title,
screen = Screen.Likes,
screen = Screen.LikedGames,
),
NEWS(
iconId = CoreR.drawable.newspaper,
titleId = FeatureNewsR.string.gaming_news_toolbar_title,
screen = Screen.News,
screen = Screen.GamingNews,
),
SETTINGS(
iconId = CoreR.drawable.cog_outline,
Expand Down
111 changes: 48 additions & 63 deletions app/src/main/java/com/paulrybitskyi/gamedge/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,72 +24,57 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.navigation.NavController
import com.paulrybitskyi.gamedge.core.utils.toCsv
import java.net.URLEncoder

internal val START_SCREEN = Screen.Discover

internal sealed class Screen(val route: String) {
data object Discover : Screen("discover")
data object Likes : Screen("likes")
data object News : Screen("news")
data object Settings : Screen("settings")
data object GamesSearch : Screen("games-search")

data object GamesCategory : Screen("games-category/{${Parameters.CATEGORY}}") {
object Parameters {
const val CATEGORY = "category"
}

fun createLink(category: String): String {
return "games-category/$category"
import com.paulrybitskyi.gamedge.feature.category.GamesCategoryRoute
import com.paulrybitskyi.gamedge.feature.discovery.GamesDiscoveryRoute
import com.paulrybitskyi.gamedge.feature.image.viewer.ImageViewerRoute
import com.paulrybitskyi.gamedge.feature.info.presentation.GameInfoRoute
import com.paulrybitskyi.gamedge.feature.likes.presentation.LikedGamesRoute
import com.paulrybitskyi.gamedge.feature.news.presentation.GamingNewsRoute
import com.paulrybitskyi.gamedge.feature.search.presentation.GamesSearchRoute
import com.paulrybitskyi.gamedge.feature.settings.presentation.SettingsRoute
import kotlin.reflect.KClass

internal val START_SCREEN = Screen.GamesDiscovery

internal sealed class Screen(val routeClass: KClass<*>) {

data object GamesDiscovery : Screen(GamesDiscoveryRoute::class)
data object LikedGames : Screen(LikedGamesRoute::class)
data object GamingNews : Screen(GamingNewsRoute::class)
data object Settings : Screen(SettingsRoute::class)
data object GamesSearch : Screen(GamesSearchRoute::class)
data object GamesCategory : Screen(GamesCategoryRoute::class) {

fun createRoute(category: String): GamesCategoryRoute {
return GamesCategoryRoute(category = category)
}
}

data object GameInfo : Screen("game-info/{${Parameters.GAME_ID}}") {
object Parameters {
const val GAME_ID = "game-id"
}
data object GameInfo : Screen(GameInfoRoute::class) {

fun createLink(gameId: Int): String {
return "game-info/$gameId"
fun createRoute(gameId: Int): GameInfoRoute {
return GameInfoRoute(gameId = gameId)
}
}

data object ImageViewer : Screen(
"image-viewer?" +
"${Parameters.TITLE}={${Parameters.TITLE}}&" +
"${Parameters.INITIAL_POSITION}={${Parameters.INITIAL_POSITION}}&" +
"${Parameters.IMAGE_URLS}={${Parameters.IMAGE_URLS}}",
) {
object Parameters {
const val TITLE = "title"
const val INITIAL_POSITION = "initial-position"
const val IMAGE_URLS = "image-urls"
}
data object ImageViewer : Screen(ImageViewerRoute::class) {

fun createLink(
title: String?,
initialPosition: Int,
fun createRoute(
imageUrls: List<String>,
): String {
val modifiedImageUrls = imageUrls
.map { imageUrl -> URLEncoder.encode(imageUrl, "UTF-8") }
.toCsv()

return buildString {
append("image-viewer?")

if (title != null) {
append("${Parameters.TITLE}=$title&")
}

append("${Parameters.INITIAL_POSITION}=$initialPosition&")
append("${Parameters.IMAGE_URLS}=$modifiedImageUrls")
}
title: String? = null,
initialPosition: Int = 0,
): ImageViewerRoute {
return ImageViewerRoute(
imageUrls = imageUrls,
title = title,
initialPosition = initialPosition,
)
}
}

val route: String
get() = routeClass.qualifiedName!!

internal companion object {

val Saver = Saver(
Expand All @@ -98,15 +83,15 @@ internal sealed class Screen(val route: String) {
)

fun forRoute(route: String): Screen {
return when (route) {
Discover.route -> Discover
Likes.route -> Likes
News.route -> News
Settings.route -> Settings
GamesSearch.route -> GamesSearch
GamesCategory.route -> GamesCategory
GameInfo.route -> GameInfo
ImageViewer.route -> ImageViewer
return when {
route.contains(GamesDiscovery.route) -> GamesDiscovery
route.contains(LikedGames.route) -> LikedGames
route.contains(GamingNews.route) -> GamingNews
route.contains(Settings.route) -> Settings
route.contains(GamesSearch.route) -> GamesSearch
route.contains(GamesCategory.route) -> GamesCategory
route.contains(GameInfo.route) -> GameInfo
route.contains(ImageViewer.route) -> ImageViewer
else -> error("Cannot find screen for the route: $route.")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class GamedgeFeaturePlugin : Plugin<Project> {
apply(libs.plugins.gamedgeAndroid.get().pluginId)
apply(libs.plugins.gamedgeJetpackCompose.get().pluginId)
apply(libs.plugins.gamedgeDaggerHilt.get().pluginId)
apply(libs.plugins.gamedgeKotlinxSerialization.get().pluginId)
}

private fun Project.addDependencies(): Unit = with(dependencies) {
Expand All @@ -26,6 +27,7 @@ class GamedgeFeaturePlugin : Plugin<Project> {
add("implementation", project(localModules.commonUi))
add("implementation", project(localModules.commonUiWidgets))
add("implementation", libs.composeHilt.get())
add("implementation", libs.composeNavigation.get())
add("implementation", libs.commonsCore.get())
add("implementation", libs.commonsKtx.get())
add("implementation", libs.coil.get())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import com.paulrybitskyi.commons.ktx.showLongToast
import com.paulrybitskyi.commons.ktx.showShortToast
import com.paulrybitskyi.gamedge.common.ui.base.BaseViewModel
import com.paulrybitskyi.gamedge.common.ui.base.events.Command
import com.paulrybitskyi.gamedge.common.ui.base.events.Route
import com.paulrybitskyi.gamedge.common.ui.base.events.Direction
import com.paulrybitskyi.gamedge.common.ui.base.events.common.GeneralCommand

@Composable
fun CommandsHandler(
viewModel: BaseViewModel,
onHandleCommand: ((Command) -> Unit)? = null,
onCommand: ((Command) -> Unit)? = null,
) {
val context = LocalContext.current

Expand All @@ -38,19 +38,19 @@ fun CommandsHandler(
when (command) {
is GeneralCommand.ShowShortToast -> context.showShortToast(command.message)
is GeneralCommand.ShowLongToast -> context.showLongToast(command.message)
else -> onHandleCommand?.invoke(command)
else -> onCommand?.invoke(command)
}
}
}
}

@Composable
fun RoutesHandler(
fun DirectionsHandler(
viewModel: BaseViewModel,
onRoute: (Route) -> Unit,
onNavigate: (Direction) -> Unit,
) {
LaunchedEffect(viewModel) {
viewModel.routeFlow
.collect(onRoute)
viewModel.directionFlow
.collect(onNavigate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package com.paulrybitskyi.gamedge.common.ui.base

import androidx.lifecycle.ViewModel
import com.paulrybitskyi.gamedge.common.ui.base.events.Command
import com.paulrybitskyi.gamedge.common.ui.base.events.Route
import com.paulrybitskyi.gamedge.common.ui.base.events.Direction
import com.paulrybitskyi.gamedge.core.markers.Loggable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
Expand All @@ -29,16 +29,16 @@ abstract class BaseViewModel : ViewModel(), Loggable {
override val logTag: String = javaClass.simpleName

private val commandChannel = Channel<Command>(Channel.BUFFERED)
private val routeChannel = Channel<Route>(Channel.BUFFERED)
private val directionChannel = Channel<Direction>(Channel.BUFFERED)

val commandFlow: Flow<Command> = commandChannel.receiveAsFlow()
val routeFlow: Flow<Route> = routeChannel.receiveAsFlow()
val directionFlow: Flow<Direction> = directionChannel.receiveAsFlow()

protected fun dispatchCommand(command: Command) {
commandChannel.trySend(command)
}

protected fun route(route: Route) {
routeChannel.trySend(route)
protected fun navigate(direction: Direction) {
directionChannel.trySend(direction)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package com.paulrybitskyi.gamedge.common.ui.base.events

interface Route
interface Direction

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@

package com.paulrybitskyi.gamedge.feature.category

import com.paulrybitskyi.gamedge.common.ui.base.events.Route
import com.paulrybitskyi.gamedge.common.ui.base.events.Direction

sealed class GamesCategoryRoute : Route {
data class Info(val gameId: Int) : GamesCategoryRoute()
data object Back : GamesCategoryRoute()
sealed class GamesCategoryDirection : Direction {
data class Info(val gameId: Int) : GamesCategoryDirection()
data object Back : GamesCategoryDirection()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.paulrybitskyi.gamedge.feature.category

import kotlinx.serialization.Serializable

@Serializable
data class GamesCategoryRoute(
val category: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.hilt.navigation.compose.hiltViewModel
import com.paulrybitskyi.gamedge.common.ui.CommandsHandler
import com.paulrybitskyi.gamedge.common.ui.RoutesHandler
import com.paulrybitskyi.gamedge.common.ui.base.events.Route
import com.paulrybitskyi.gamedge.common.ui.DirectionsHandler
import com.paulrybitskyi.gamedge.common.ui.base.events.Direction
import com.paulrybitskyi.gamedge.common.ui.theme.GamedgeTheme
import com.paulrybitskyi.gamedge.common.ui.widgets.AnimatedContentContainer
import com.paulrybitskyi.gamedge.common.ui.widgets.FiniteUiState
Expand All @@ -53,20 +53,25 @@ import com.paulrybitskyi.gamedge.feature.category.widgets.rememberGamesGridConfi
import com.paulrybitskyi.gamedge.core.R as CoreR

@Composable
fun GamesCategoryScreen(onRoute: (Route) -> Unit) {
fun GamesCategoryScreen(
route: GamesCategoryRoute,
onNavigate: (Direction) -> Unit,
) {
GamesCategoryScreen(
viewModel = hiltViewModel(),
onRoute = onRoute,
viewModel = hiltViewModel<GamesCategoryViewModel, GamesCategoryViewModel.Factory>(
creationCallback = { factory -> factory.create(route) },
),
onNavigate = onNavigate,
)
}

@Composable
private fun GamesCategoryScreen(
viewModel: GamesCategoryViewModel,
onRoute: (Route) -> Unit,
onNavigate: (Direction) -> Unit,
) {
CommandsHandler(viewModel = viewModel)
RoutesHandler(viewModel = viewModel, onRoute = onRoute)
DirectionsHandler(viewModel = viewModel, onNavigate = onNavigate)
GamesCategoryScreen(
uiState = viewModel.uiState.collectAsState().value,
onBackButtonClicked = viewModel::onToolbarLeftButtonClicked,
Expand Down
Loading

0 comments on commit 36f607d

Please sign in to comment.