Last active
March 15, 2024 12:57
-
-
Save IlyaLavrov97/afed88cfc77270d9e838543b61382edd to your computer and use it in GitHub Desktop.
Compose Navigation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract class NavRoute<T : BaseNavigationViewModel> { | |
abstract val destination: NavigationDestination | |
@Composable | |
open fun Content(viewModel: T) = Unit | |
@Composable | |
abstract fun viewModel(): T | |
open fun getArguments(): List<NamedNavArgument> = listOf() | |
open fun getDeepLinks(): List<NavDeepLink> = listOf() | |
fun buildDestination( | |
builder: NavGraphBuilder, | |
navHostController: NavHostController, | |
sharedViewModel: SharedStateViewModel, | |
sheetGesturesEnabled: Boolean = true, | |
) { | |
if (destination.isBottomSheet) { | |
bottomSheet(builder, navHostController, sharedViewModel, sheetGesturesEnabled) | |
} else { | |
composable(builder, navHostController, sharedViewModel) | |
} | |
} | |
private fun composable( | |
builder: NavGraphBuilder, | |
navHostController: NavHostController, | |
sharedViewModel: SharedStateViewModel, | |
) { | |
builder.composable( | |
destination.route, | |
getArguments(), | |
getDeepLinks(), | |
enterTransition = { fadeIn() }, | |
exitTransition = { fadeOut() } | |
) { | |
val viewModel = viewModel() | |
sharedViewModel.navigationViewModel = viewModel | |
viewModel.composableScope = sharedViewModel.composableScope | |
val navigationState by viewModel.navigationState.collectAsStateLifecycleAware() | |
LaunchedEffect(navigationState) { | |
updateNavigationState( | |
sharedViewModel, | |
navHostController, | |
navigationState, | |
viewModel::onNavigated | |
) | |
if (navigationState == NavigationState.Idle) { | |
sharedViewModel.updateStateWithDestination(destination) | |
} | |
} | |
GetContentWithToolbar( | |
stackEntry = it, | |
viewModel = viewModel, | |
withToolbar = destination.toolbarState != null | |
) | |
} | |
} | |
@OptIn(ExperimentalMaterialNavigationApi::class) | |
private fun bottomSheet( | |
builder: NavGraphBuilder, | |
navHostController: NavHostController, | |
sharedViewModel: SharedStateViewModel, | |
sheetGesturesEnabled: Boolean = true | |
) { | |
builder.bottomSheet(destination.route, getArguments()) { | |
val viewModel = viewModel() | |
viewModel.composableScope = sharedViewModel.composableScope | |
val navigationState by viewModel.navigationState.collectAsStateLifecycleAware() | |
sharedViewModel.updateBottomSheetState(BottomSheetState(sheetGesturesEnabled)) | |
LaunchedEffect(navigationState) { | |
updateNavigationState( | |
sharedViewModel, | |
navHostController, | |
navigationState, | |
viewModel::onNavigated | |
) | |
if (navigationState == NavigationState.Idle) { | |
sharedViewModel.updateStateWithDestination(destination) | |
} | |
} | |
GetContentWithToolbar( | |
stackEntry = it, | |
viewModel = viewModel, | |
withToolbar = false | |
) | |
} | |
} | |
@Composable | |
open fun GetContentWithToolbar( | |
stackEntry: NavBackStackEntry, | |
viewModel: T, | |
withToolbar: Boolean | |
) { | |
if (withToolbar) { | |
Box( | |
modifier = Modifier.padding( | |
top = PayCommonDimens.toolbarHeight | |
) | |
) { | |
Content(viewModel) | |
} | |
} else { | |
Content(viewModel) | |
} | |
} | |
private fun updateNavigationState( | |
sharedViewModel: SharedStateViewModel, | |
navHostController: NavHostController, | |
navigationState: NavigationState, | |
onNavigated: (navState: NavigationState) -> Unit, | |
) { | |
when (navigationState) { | |
is NavigationState.NavigateToRoute -> { | |
val currentRouteInfo = sharedViewModel.stateFlow.value.currentRoute | |
val newRouteInfo = navigationState.route | |
if (navigationState.allowSameRoute || currentRouteInfo?.value != newRouteInfo.value) { | |
if (newRouteInfo.value != null) | |
navHostController.navigate(newRouteInfo.value) { | |
if (navigationState.clearBackStack) popUpTo(0) | |
} | |
} | |
onNavigated(navigationState) | |
} | |
is NavigationState.PopToRoute -> { | |
navHostController.popBackStack(navigationState.staticRoute, false) | |
onNavigated(navigationState) | |
} | |
is NavigationState.PopBackStack -> { | |
navHostController.popBackStack() | |
onNavigated(navigationState) | |
} | |
is NavigationState.Idle -> Unit | |
} | |
} | |
} | |
abstract class NavRouteWithArgs<T : BaseNavigationViewModel, NavArg : Any> : NavRoute<T>() { | |
abstract fun getViewModelNavArgs(args: Map<String, Any?>): NavArg? | |
@Composable | |
override fun GetContentWithToolbar( | |
stackEntry: NavBackStackEntry, | |
viewModel: T, | |
withToolbar: Boolean | |
) { | |
if (withToolbar) { | |
Box( | |
modifier = Modifier.padding( | |
top = PayCommonDimens.toolbarHeight | |
) | |
) { | |
GetContentUsingArgs(stackEntry, viewModel) | |
} | |
} else { | |
GetContentUsingArgs(stackEntry, viewModel) | |
} | |
} | |
@Composable | |
private fun GetContentUsingArgs( | |
stackEntry: NavBackStackEntry, | |
viewModel: T, | |
) { | |
val args = stackEntry.arguments | |
if (args == null || getArguments().isEmpty()) { | |
Content(viewModel) | |
} else { | |
val resultArgs = mutableMapOf<String, Any?>() | |
for (argName in getArguments().map { namedArg -> namedArg.name }) { | |
if (args.containsKey(argName)) { | |
resultArgs[argName] = args.getString(argName) | |
} | |
} | |
viewModel.navArg = getViewModelNavArgs(resultArgs) | |
Content(viewModel) | |
} | |
} | |
} | |
sealed class NavigationState { | |
data object Idle : NavigationState() | |
data class NavigateToRoute( | |
val route: RouteInfo, | |
val clearBackStack: Boolean, | |
val allowSameRoute: Boolean = true, | |
val id: String = UUID.randomUUID().toString() | |
) : NavigationState() | |
data class PopToRoute(val staticRoute: String, val id: String = UUID.randomUUID().toString()) : | |
NavigationState() | |
data class PopBackStack(val id: String = UUID.randomUUID().toString()) : NavigationState() | |
} | |
interface RouteNavigator { | |
fun onNavigated(state: NavigationState) | |
fun popToRoute(route: String) | |
fun popBackStack() | |
fun navigateToRoute(destination: NavigationDestination, allowSameRoute: Boolean = true) | |
val navigationState: StateFlow<NavigationState> | |
var navArg: Any? | |
} | |
class LastNavEventRouteNavigator @Inject constructor() : RouteNavigator { | |
override val navigationState: MutableStateFlow<NavigationState> = | |
MutableStateFlow(NavigationState.Idle) | |
override var navArg: Any? = null | |
override fun onNavigated(state: NavigationState) { | |
// clear navigation state, if state is the current state: | |
navigationState.compareAndSet(state, NavigationState.Idle) | |
} | |
override fun popToRoute(route: String) = navigate(NavigationState.PopToRoute(route)) | |
override fun popBackStack() = navigate(NavigationState.PopBackStack()) | |
override fun navigateUp() = navigate(NavigationState.NavigateUp()) | |
override fun navigateToRoute(destination: NavigationDestination, allowSameRoute: Boolean) { | |
processEvent { | |
navigate( | |
NavigationState.NavigateToRoute( | |
RouteInfo( | |
value = destination.route, | |
isBottomSheet = destination.isBottomSheet | |
), | |
destination.clearBackStack, | |
allowSameRoute = allowSameRoute | |
) | |
) | |
} | |
} | |
@VisibleForTesting | |
fun navigate(state: NavigationState) { | |
navigationState.value = state | |
} | |
} | |
// Implementation of NavRoute | |
@MyRoute | |
object CustomScreenRoute : NavRoute<CustomScreenViewModel>() { | |
override val destination: NavigationDestination = CustomNavigationDestinations.CustomScreen | |
@Composable | |
override fun viewModel(): CustomScreenViewModel = hiltViewModel() | |
@Composable | |
override fun Content(viewModel: CustomScreenViewModel) = SettingsScreen(viewModel) | |
} | |
// Example of navigation | |
viewModel.navigateToRoute(CustomNavigationDestinations.CustomScreen) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment