Skip to content

Instantly share code, notes, and snippets.

@IlyaLavrov97
Last active March 15, 2024 12:57
Show Gist options
  • Save IlyaLavrov97/afed88cfc77270d9e838543b61382edd to your computer and use it in GitHub Desktop.
Save IlyaLavrov97/afed88cfc77270d9e838543b61382edd to your computer and use it in GitHub Desktop.
Compose Navigation
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