October 2023
Especially in Compose
@Composable
fun Screen() {
Column {
TabNavHost()
BottomMenu(
items = [Tab.HOME, /*...*/],
)
}
}
@Composable
fun Screen() {
val tabNavViewModel: TabNavViewModel = viewModel()
Column {
TabNavHost()
BottomMenu(
items = [Tab.HOME, /*...*/],
)
}
}
@Composable
fun Screen() {
val tabNavViewModel: TabNavViewModel = viewModel()
Column {
TabNavHost(
viewModel = tabNavViewModel
)
BottomMenu(
items = [Tab.HOME, /*...*/],
)
}
}
@Composable
fun Screen() {
val tabNavViewModel: TabNavViewModel = viewModel()
Column {
TabNavHost(
viewModel = tabNavViewModel
)
BottomMenu(
items = [Tab.HOME, /*...*/],
selectedItem = tabNavViewModel.current
)
}
}
@Composable
fun Screen() {
val tabNavViewModel: TabNavViewModel = viewModel()
Column {
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()
}
class TabGraph(
val tabToContentMap: Map<String, @Composable () -> Unit>,
) { /*...*/ }
TabNavHost(viewModel) {
tab(Tab.HOME) {
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>,
)
class TabGraphBuilder() {
fun tab(
name: String,
content: @Composable () -> 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)
}
val content = graph.getContentComposable(current)
content()
AnimatedContent(
state = current,
) { it ->
val content = graph.getContentComposable(it)
content()
}
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
OnBackPressedDispatcherOwner
LocalOnBackPressedDispatcherOwner
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 */
}
ViewModelStoreOwner
LocalViewModelStoreOwner
SaveableStateRegistry
LocalSaveableStateRegistry
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()
}
}
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()
}
}
🐘
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
Will likely work for most of us,
most of the time
Navigation, most of the time, is orthogonal to the core logic of the UI
Will likely work for most of us,
most of the time
But is likely NOT going to work for most of us,
some of the time
Because, some of the time, navigation is very much the main logic of the UI
🙅
But, some of the time, navigation is very much the main logic of the UI
👍
The cost to
Take control of navigation in our apps
is
And our apps are only getting more sophisticated and unique
It's time to
Take control of navigation in our apps
Thank you 🙏
Questions❓🤔🙋♀️