Skip to content

Commit

Permalink
Merge pull request #7 from truyentd-2427/optimize_navigation
Browse files Browse the repository at this point in the history
Optimize navigation
  • Loading branch information
truyentd-2427 authored Jun 21, 2024
2 parents c43e2fc + 24b0d4b commit dc5a9a1
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 63 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.truyentd.moviecompose.navigation

import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.runtime.Composable
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import com.truyentd.moviecompose.navigation.auth.authNavGraph
import com.truyentd.moviecompose.navigation.movie.movieNavGraph
import com.truyentd.moviecompose.navigation.top.topNavGraph
import com.truyentd.moviecompose.presentation.screens.moviedetail.MovieDetailScreen

sealed class AppNavGraph(val route: String) {
object Root : AppNavGraph(route = "root-graph")
Expand All @@ -32,3 +37,64 @@ fun AppNavHost(navController: NavHostController) {
movieNavGraph(navController)
}
}

fun NavGraphBuilder.composable(
destination: BaseDestination,
enterTransition: (@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
exitTransition: (@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
popEnterTransition: (@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
enterTransition,
popExitTransition: (@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
exitTransition,
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
composable(
route = destination.route,
arguments = destination.arguments,
deepLinks = destination.deepLinks.map {
navDeepLink {
uriPattern = it
}
},
enterTransition = enterTransition,
exitTransition = exitTransition,
popEnterTransition = popEnterTransition,
popExitTransition = popExitTransition,
content = content
)
}

/**
* Navigate to provided [BaseDestination] with a Pair of key value String and Data [parcel]
* Caution to use this method. This method use savedStateHandle to store the Parcelable data.
* When previousBackstackEntry is popped out from navigation stack, savedStateHandle will return null and cannot retrieve data.
* eg.Login -> Home, the Login screen will be popped from the back-stack on logging in successfully.
*/
fun NavHostController.navigate(destination: BaseDestination, parcel: Pair<String, Any?>? = null) {
when (destination) {
is BaseDestination.Up -> {
destination.results.forEach { (key, value) ->
previousBackStackEntry?.savedStateHandle?.set(key, value)
}
navigateUp()
}

is BaseDestination.PopToBackStack -> {
destination.results.forEach { (key, value) ->
previousBackStackEntry?.savedStateHandle?.set(key, value)
}
popBackStack(destination.targetDestination.route, inclusive = destination.inclusive)
}

else -> {
parcel?.let { (key, value) ->
currentBackStackEntry?.savedStateHandle?.set(key, value)
}
navigate(route = destination.destination)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.truyentd.moviecompose.navigation

import androidx.navigation.NamedNavArgument

abstract class BaseDestination(val route: String = "") {
open val arguments: List<NamedNavArgument> = emptyList()

open val deepLinks: List<String> = listOf(
"https://moviecompose.com/$route",
)

open var destination: String = route

open var parcelableArgument: Pair<String, Any?> = "" to null


data class Up(val results: HashMap<String, Any> = hashMapOf()) : BaseDestination() {
fun addResult(key: String, value: Any) = apply {
results[key] = value
}
}

data class PopToBackStack(
val targetDestination: BaseDestination,
val inclusive: Boolean = false,
val results: HashMap<String, Any> = hashMapOf(),
) : BaseDestination() {
fun addResult(key: String, value: Any) = apply {
results[key] = value
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.truyentd.moviecompose.navigation.auth

import com.truyentd.moviecompose.navigation.AppDestination
import com.truyentd.moviecompose.navigation.BaseDestination

sealed class AuthDestination(route: String): AppDestination(route) {
object Login : AppDestination(route = "login")
}
sealed class AuthDestination(route: String): BaseDestination(route) {
object Login : BaseDestination(route = "login")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ package com.truyentd.moviecompose.navigation.auth

import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.truyentd.moviecompose.navigation.AppNavGraph
import com.truyentd.moviecompose.navigation.composable
import com.truyentd.moviecompose.presentation.screens.login.LoginScreen

fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation(
route = AppNavGraph.Auth.route,
startDestination = AuthDestination.Login.route
) {
composable(AuthDestination.Login.route) {
composable(destination = AuthDestination.Login) {
LoginScreen()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
package com.truyentd.moviecompose.navigation.movie

sealed class MovieDestination(val route: String) {
object MovieDetail: MovieDestination("movie-detail/{movieId}")
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.truyentd.moviecompose.navigation.BaseDestination

sealed class MovieDestination {
object MovieDetail : BaseDestination("movie/{movieId}") {
override val arguments = listOf(
navArgument("movieId") { type = NavType.StringType }
)

fun createRoute(id: String) = apply {
destination = "movie/$id"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@ package com.truyentd.moviecompose.navigation.movie

import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import com.truyentd.moviecompose.navigation.AppNavGraph
import com.truyentd.moviecompose.navigation.composable
import com.truyentd.moviecompose.presentation.screens.moviedetail.MovieDetailScreen

fun NavGraphBuilder.movieNavGraph(navController: NavHostController) {
Expand All @@ -16,11 +13,7 @@ fun NavGraphBuilder.movieNavGraph(navController: NavHostController) {
startDestination = MovieDestination.MovieDetail.route
) {
composable(
route = MovieDestination.MovieDetail.route,
arguments = listOf(navArgument("movieId") { type = NavType.StringType }),
deepLinks = listOf(navDeepLink {
uriPattern = "https://moviecompose.com/movie-detail/{movieId}"
})
destination = MovieDestination.MovieDetail,
) {
MovieDetailScreen()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package com.truyentd.moviecompose.navigation.top

import androidx.annotation.DrawableRes
import com.truyentd.moviecompose.R
import com.truyentd.moviecompose.navigation.AppDestination
import com.truyentd.moviecompose.navigation.BaseDestination

sealed class TopDestination(
route: String,
@DrawableRes val unselectedIcon: Int,
@DrawableRes val selectedIcon: Int,
) : AppDestination(route) {
) : BaseDestination(route) {
object Home : TopDestination(
route = "home",
unselectedIcon = R.drawable.ic_film_unselected,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,56 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import com.truyentd.moviecompose.data.model.MovieData
import com.truyentd.moviecompose.navigation.AppNavGraph
import com.truyentd.moviecompose.navigation.movie.MovieDestination
import com.truyentd.moviecompose.navigation.BaseDestination
import com.truyentd.moviecompose.navigation.composable
import com.truyentd.moviecompose.navigation.navigate
import com.truyentd.moviecompose.presentation.screens.favorite.FavoriteScreen
import com.truyentd.moviecompose.presentation.screens.home.HomeScreen
import com.truyentd.moviecompose.presentation.screens.main.TopScreen
import com.truyentd.moviecompose.presentation.screens.search.SearchScreen

fun NavGraphBuilder.topNavGraph(navController: NavHostController) {
composable(AppNavGraph.Top.route) {
composable(route = AppNavGraph.Top.route) {
TopScreen(
onMovieClick = { movie ->
navController.navigate("movie-detail/${movie.id}")
},
onFavoriteClick = {
navController.navigate(MovieDestination.MovieDetail.route)
navigator = { destination ->
navController.navigate(destination)
},
)
}
}


@Composable
fun TopNavHost(
navController: NavHostController,
modifier: Modifier,
onMovieClick: (MovieData) -> Unit,
onFavoriteClick: () -> Unit,
navigator: ((BaseDestination) -> Unit)? = null,
) {
NavHost(
navController = navController,
modifier = modifier,
startDestination = TopDestination.Home.route
) {
composable(
route = TopDestination.Home.route,
destination = TopDestination.Home,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
) {
HomeScreen(onMovieClick = onMovieClick)
HomeScreen(navigator = navigator)
}
composable(
route = TopDestination.Search.route,
destination = TopDestination.Search,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
) {
SearchScreen()
}
composable(
route = TopDestination.Favorite.route,
deepLinks = listOf(navDeepLink {
uriPattern = "https://moviecompose.com/favorite"
}),
destination = TopDestination.Favorite,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
) {
FavoriteScreen {
onFavoriteClick.invoke()
}
FavoriteScreen()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.truyentd.moviecompose.presentation.screens.favorite

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
Expand All @@ -21,15 +20,15 @@ fun SettingScreenPreview() {

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FavoriteScreen(onFavoriteClick: (() -> Unit)? = null) {
fun FavoriteScreen() {
Scaffold(containerColor = AppColors.White) { innerPadding ->
Box(
modifier = Modifier.fillMaxSize().padding(innerPadding),
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Text(text = "Favorite Screen", modifier = Modifier.clickable {
onFavoriteClick?.invoke()
})
Text(text = "Favorite Screen")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.truyentd.moviecompose.R
import com.truyentd.moviecompose.data.model.MovieData
import com.truyentd.moviecompose.navigation.BaseDestination
import com.truyentd.moviecompose.navigation.movie.MovieDestination
import com.truyentd.moviecompose.presentation.components.LoadingBox
import com.truyentd.moviecompose.presentation.components.SectionTitle
import com.truyentd.moviecompose.presentation.screens.home.components.NowShowingMovieItem
Expand All @@ -48,19 +50,19 @@ fun HomeScreenPreview() {
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
onMovieClick: ((MovieData) -> Unit)? = null
navigator: ((BaseDestination) -> Unit)? = null
) {
val uiState: HomeUiState by viewModel.uiState.collectAsStateWithLifecycle()
val isLoading: Boolean by viewModel.isLoading.collectAsStateWithLifecycle()

HomeScreenContent(uiState = uiState, isLoading = isLoading, onMovieClick = onMovieClick)
HomeScreenContent(uiState = uiState, isLoading = isLoading, navigator = navigator)
}

@Composable
private fun HomeScreenContent(
uiState: HomeUiState,
isLoading: Boolean,
onMovieClick: ((MovieData) -> Unit)? = null
navigator: ((BaseDestination) -> Unit)? = null
) {
Column(
modifier = Modifier
Expand All @@ -86,7 +88,12 @@ private fun HomeScreenContent(
},
)
Spacer(modifier = Modifier.height(16.dp))
ListNowShowingMovies(uiState.nowPlayingMovies, onMovieClick = onMovieClick)
ListNowShowingMovies(
movies = uiState.nowPlayingMovies,
onMovieClick = {
navigator?.invoke(MovieDestination.MovieDetail.createRoute(it.id.toString()))
},
)
Spacer(modifier = Modifier.height(24.dp))
SectionTitle(
title = stringResource(id = R.string.popular),
Expand All @@ -95,7 +102,12 @@ private fun HomeScreenContent(
},
)
Spacer(modifier = Modifier.height(16.dp))
LisPopularMovies(uiState.popularMovies, onMovieClick = onMovieClick)
LisPopularMovies(
movies = uiState.popularMovies,
onMovieClick = {
navigator?.invoke(MovieDestination.MovieDetail.createRoute(it.id.toString()))
},
)
Spacer(modifier = Modifier.height(24.dp))
}
}
Expand Down
Loading

0 comments on commit dc5a9a1

Please sign in to comment.