🧭 Building a navigation framework in Compose

okmanideep

What does it take to build a navigation framework?

Especially in Compose

							
								@Composable
								fun Screen() {
									Column(modifier = Modifier.fillMaxSize()) {
										TabNavHost()
										BottomMenu(
											items = [Tab.HOME, /*...*/],
										)
									}
								}
							
						
						
							@Composable
							fun Screen() {
								Column(modifier = Modifier.fillMaxSize()) {
									TabNavHost()
									BottomMenu(
										items = [Tab.HOME, /*...*/],
									)
								}
							}
						
					
						
							@Composable
							fun Screen() {
								val tabNavViewModel: TabNavViewModel = viewModel()
								Column(modifier = Modifier.fillMaxSize()) {
									TabNavHost()
									BottomMenu(
										items = [Tab.HOME, /*...*/],
									)
								}
							}
						
					
						
							@Composable
							fun Screen() {
								val tabNavViewModel: TabNavViewModel = viewModel()
								Column(modifier = Modifier.fillMaxSize()) {
									TabNavHost(
										viewModel = tabNavViewModel
									)
									BottomMenu(
										items = [Tab.HOME, /*...*/],
									)
								}
							}
						
					
						
							@Composable
							fun Screen() {
								val tabNavViewModel: TabNavViewModel = viewModel()
								Column(modifier = Modifier.fillMaxSize()) {
									TabNavHost(
										viewModel = tabNavViewModel
									)
									BottomMenu(
										items = [Tab.HOME, /*...*/],
										selectedItem = tabNavViewModel.current
									)
								}
							}
						
					
						
							@Composable
							fun Screen() {
								val tabNavViewModel: TabNavViewModel = viewModel()
								Column(modifier = Modifier.fillMaxSize()) {
									TabNavHost(
										viewModel = tabNavViewModel
									)
									BottomMenu(
										items = [Tab.HOME, /*...*/],
										selectedItem = tabNavViewModel.current,
										onItemSelected = { tab -> 
											tabNavViewModel.goToTab(tab)
										}
									)
								}
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
							) {
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
							) {
								val current = viewModel.current
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
							) {
								val current = viewModel.current
								// draw the content corresponding to the `current` tab
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								graph: TabGraph,
							) {
								val current = viewModel.current
								// draw the content corresponding to the `current` tab
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								graph: TabGraph,
							) {
								val current = viewModel.current
								// draw the content corresponding to the `current` tab
								val content = graph.getContentComposable(current)
								content()
							}
						
					
						
							TabNavHost(viewModel) {
								tab(Tab.HOME, isDefault = true) {
									HomeTabUI()
								}
								
								tab(Tab.Search) {
									SearchTabUI()
								}

								tab(Tab.Profile) {
									ProfileTabUI()
								}
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								builder: TabNavGraphBuilder.() -> Unit,
							) {
							}
						
					
						
							class TabGraph(
								val tabToContentMap: Map<String, @Composable () -> Unit>,
								val defaultTab: String,
							)
							
							class TabGraphBuilder() {
								fun tab(
									name: String,
									isDefault: Boolean = false,
									content: @Composable () -> Unit,
								) {
									/*... */
								}
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								builder: TabNavGraphBuilder.() -> Unit,
							) {
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								builder: TabNavGraphBuilder.() -> Unit,
							) {
								val graph = remember(builder) {
									TabNavGraphBuilder().apply(builder).build() 
								}
								TabNavHost(viewModel, graph)
							}
						
					
							
								@Composable
								fun Screen() {
									val viewModel: TabNavViewModel = viewModel()

									Column(modifier = Modifier.fillMaxSize()) {
										TabNavHost(viewModel) { /*...*/ }

										BottomMenu(
											items = [Tab.HOME, /*...*/],
											selectedTab = viewModel.current
											onItemSelected = {
												tab -> viewModel.goToTab(tab)
											}
										)
									}
								}
							
						
						
							val content = graph.getContentComposable(current)
							content()
						
					
						
							AnimatedContent(
								state = current,
							) { it -> 
								val content = graph.getContentComposable(it)
								content()
							}
						
					
							
								@Composable
								fun Screen() {
									val viewModel: TabNavViewModel = viewModel()

									Column(modifier = Modifier.fillMaxSize()) {
										TabNavHost(viewModel) { /*...*/ }

										BottomMenu(
											items = [Tab.HOME, /*...*/],
											selectedTab = viewModel.current
											onItemSelected = {
												tab -> viewModel.goToTab(tab)
											}
										)
									}
								}
							
						
Are we done?
Key Aspects of a navigation framework
Stack
Scope
Lifecycle


to the rescue

CompositionLocal
						
							@Composable
							fun ThemesDemo() {
								Column {
									MaterialTheme(colors = lightColors) {
										DemoUI() // appears in light theme
									}
									MaterialTheme(colors = darkColors) {
										DemoUI() // appears in dark theme
									}
								}
							}
						
					
						
							@Composable
							fun ThemesDemo() {
								Column {
									CompositionLocalProvider(LocalColors provides lightColors) {
										DemoUI() // appears in light theme
									}
									CompositionLocalProvider(LocalColors provides darkColors) {
										DemoUI() // appears in dark theme
									}
								}
							}
						
					
						
							@Composable
							fun ThemesDemo() {
								Column {
									CompositionLocalProvider(LocalColors provides lightColors) {
										DemoUI() // appears in light theme
									}
									CompositionLocalProvider(LocalColors provides darkColors) {
										DemoUI() // appears in dark theme
									}
								}
							}

							/* Using LocalColors */
							val buttonBgColor = LocalColors.current.primary
						
					
Key Aspects of a navigation framework
Stack
Scope
Lifecycle
Stack


OnBackPressedDispatcherOwner
LocalOnBackPressedDispatcherOwner

Stack


BackHandler

						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								graph: TabGraph
							) {
								
								/* draw current tab content */
							}
						
					
						
							@Composable
							fun TabNavHost(
								viewModel: TabNavViewModel,
								graph: TabGraph
							) {
								BackHandler(enabled = viewModel.canGoBack()) {
									viewModel.goBack()
								}
								
								/* draw current tab content */
							}
						
					
Key Aspects of a navigation framework
Stack
Scope
Lifecycle
Key Aspects of a navigation framework
Stack
Scope
Lifecycle
Scope


ViewModelStoreOwner
LocalViewModelStoreOwner
SaveableStateRegistry
LocalSaveableStateRegistry

Scope


ViewModelStoreOwner
LocalViewModelStoreOwner
SaveableStateHolder
rememberSaveableStateHolder()

						
							/* TabNavHost */

							graph.getContent(viewModel.current).invoke()
						
					
						
							/* TabNavHost */

							val current = viewModel.current
							CompositionLocalProvider(
							) {
								graph.getContent(current).invoke()
							}
						
					
						
							/* TabNavHost */

							val current = viewModel.current
							val vmStoreOwner = viewModel.getVMStoreOwnerFor(current)
							CompositionLocalProvider(
								LocalViewModelStoreOwner provides vmStoreOwner
							) {
								graph.getContent(current).invoke()
							}
						
					
						
							/* TabNavHost */
							val saveableStateHolder = rememberSaveableStateHolder()

							val current = viewModel.current
							val vmStoreOwner = viewModel.getVMStoreOwnerFor(current)
							CompositionLocalProvider(
								LocalViewModelStoreOwner provides vmStoreOwner
							) {
								saveableStateHolder.SaveableStateProvider(key = current) {
									graph.getContent(current).invoke()
								}
							}
						
					
Key Aspects of a navigation framework
Stack
Scope
Lifecycle
Key Aspects of a navigation framework
Stack
Scope
Lifecycle
Lifecycle


LifecycleOwner
LocalLifeCycleOwner

						
							/* TabNavHost */
							val saveableStateHolder = rememberSaveableStateHolder()

							val current = viewModel.current
							val vmStoreOwner = viewModel.getVMStoreOwnerFor(current)
							CompositionLocalProvider(
								LocalViewModelStoreOwner provides vmStoreOwner,
							) {
								saveableStateHolder.SaveableStateProvider(key = current) {
									graph.getContent(current).invoke()
								}
							}
						
					
						
							/* TabNavHost */
							val saveableStateHolder = rememberSaveableStateHolder()

							val current = viewModel.current
							val vmStoreOwner = viewModel.getVMStoreOwnerFor(current)
							val lifecycleOwner = viewModel.getLifecycleOwnerFor(current)
							CompositionLocalProvider(
								LocalViewModelStoreOwner provides vmStoreOwner,
								LocalLifeCycleOwner provides lifecycleOwner,
							) {
								saveableStateHolder.SaveableStateProvider(key = current) {
									graph.getContent(current).invoke()
								}
							}
						
					
						
							class TabEntry(
								val name: String,
							)
						
					
						
							class TabEntry(
								val name: String,
							): ViewModelStoreOwner, LifecycleOwner
						
					
						
							/* TabNavHost */
							val saveableStateHolder = rememberSaveableStateHolder()

							val current = viewModel.current
							val vmStoreOwner = viewModel.getVMStoreOwnerFor(current)
							val lifecycleOwner = viewModel.getLifecycleOwnerFor(current)
							CompositionLocalProvider(
								LocalViewModelStoreOwner provides vmStoreOwner,
								LocalLifeCycleOwner provides lifecycleOwner,
							) {
								saveableStateHolder.SaveableStateProvider(key = current) {
									graph.getContent(current).invoke()
								}
							}
						
					
						
							/* TabNavHost */
							val saveableStateHolder = rememberSaveableStateHolder()

							val current = viewModel.currentEntry
							CompositionLocalProvider(
								LocalViewModelStoreOwner provides current,
								LocalLifeCycleOwner provides current,
							) {
								saveableStateHolder.SaveableStateProvider(key = current.name) {
									graph.getContent(current.name).invoke()
								}
							}
						
					
Key Aspects of a navigation framework
Stack
Scope
Lifecycle
Key Aspects of a navigation framework
Stack
Scope
Lifecycle

🐘

androidx.navigation:navigation-compose

						
							/* bottom nav onClick */
							navController.navigate(screen.route) {
								// Pop up to the start destination of the graph to
								// avoid building up a large stack of destinations
								// on the back stack as users select items
								popUpTo(navController.graph.findStartDestination().id) {
									saveState = true
								}
								// Avoid multiple copies of the same destination when
								// reselecting the same item
								launchSingleTop = true
								// Restore state when reselecting a previously selected item
								restoreState = true
							}
						
					
d.android.com/jetpack/compose/navigation#bottom-nav
						
							/* bottom nav onClick */
							navController.navigate(screen.route) {
								popUpTo(navController.graph.findStartDestination().id) {
									saveState = true
								}

								launchSingleTop = true
								restoreState = true
							}
						
					
d.android.com/jetpack/compose/navigation#bottom-nav
d.android.com/jetpack/compose/navigation

Will likely work for most of us,
most of the time

And is likely NOT going to work for most of us,
some of the time

Navigation, most of the time, is orthogonal to the core logic of the UI

UI

NavFramework

But, some of the time, navigation is very much the main logic of the UI

ViewModel

UI

NavFramework

🙅

But, some of the time, navigation is very much the main logic of the UI

ViewModel

UI

👍

The cost to
Take control of navigation in our apps
is

  • Lower because of
  • Even lower because of compose

And our apps are only getting more complicated

It's time to
Take control of navigation in our apps

🤔❓🙋‍♀️

Thank you 🙏

MANIDEEP POLIREDDI