Jetpack Compose: navigate to X screen (jump) keeping back-stack history - android

Have Activity which holds NavHostController, when activity starts, need to navigate X screen, but pressing back need to navigate to previous screen, not to the start destination (because navigating to X was from startDest).
#OptIn(ExperimentalMaterialNavigationApi::class)
#Composable
private fun NavHostController(
navController: NavHostController,
startDest: String = "screen 1"
) {
NavHost(
navController = navController,
startDestination = startDest,
) {
composable("screen 1") {}
composable("screen 2") {}
...
composable("screen N") {}
}}
EXAMPLE:
have screens like: "screen 1-2-3-4-5..N"
and the "screen 1" is start destination,
activity starts and nav-ctrl jumps to screen 5(for example),
and the back press need to work like: to 4, 3, 2.. NOT direct 1 (as start destination is 1)
Question:
How to set back stack history in nav controller
OR
How navigate (jump) with keeping back stack order.

To achieve this you will need to use the BackHandler and make some check to see if the previous back stack route is the screen that should be the destination when using the back button. For example:
#Composable
fun ScreenOne(
nextScreen: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "ScreenOne")
Button(onClick = nextScreen) {
Text(text = "Next")
}
}
}
#Composable
fun ScreenTwo(
back: () -> Unit,
nextScreen: () -> Unit
) {
// to intercept the click on the back button of the android navigation bar
BackHandler(onBack = back)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "ScreenTwo")
Button(onClick = back) {
Text(text = "Back")
}
Button(onClick = nextScreen) {
Text(text = "Next")
}
}
}
#Composable
fun ScreenThree(
back: () -> Unit
) {
// to intercept the click on the back button of the android navigation bar
BackHandler(onBack = back)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "ScreenThree")
Button(onClick = back) {
Text(text = "Back")
}
}
}
ScreenOne just have the option to navigate to ScreenTwo.
ScreenTwo have the option to navigate (back) to ScreenOne and navigate to ScreenThree.
ScreenThree just have the option to navigate (back) to ScreenTwo.
Extension to navigate back:
fun NavHostController.navigateBack(
targetRoute: String,
currentRoute: String
) {
val previousRoute = previousBackStackEntry?.destination?.route ?: "null"
// if the previous route is what we want, just go back
if (previousRoute == targetRoute) popBackStack()
// otherwise, we do the navigation explicitly
else navigate(route = targetRoute) {
// remove the entire backstack up to this this route, including herself
popUpTo(route = currentRoute) { inclusive = true }
launchSingleTop = true
}
}
NavHost:
NavHost(
modifier = Modifier.fillMaxSize(),
navController = navController,
startDestination = "screen_three"
) {
composable(route = "screen_one") {
ScreenOne(
nextScreen = {
navController.navigate(route = "screen_two") {
launchSingleTop = true
}
}
)
}
composable(route = "screen_two") {
ScreenTwo(
back = {
navController.navigateBack(
targetRoute = "screen_one",
currentRoute = "screen_two"
)
},
nextScreen = {
navController.navigate(route = "screen_three") {
launchSingleTop = true
}
}
)
}
composable(route = "screen_three") {
ScreenThree(
back = {
navController.navigateBack(
targetRoute = "screen_two",
currentRoute = "screen_three"
)
}
)
}
}

Related

How navigate back with new argument in Jetpack Compose?

I want to navigate back with different input argument.
I have a ParentScreen and it is using input argument arg (="first"). Code navigates from parent to child by sending newArg (="firstsecond") on button press. When navigating back from child to parent I want parent's input arg to be "fistsecond".
Below I wrote the code (it is not working) to show what I want.
#Composable
fun ParentScreen(
nav: NavHostController,
arg: String = "first"
) {
val newArg = arg + "second"
Button(onClick = {
nav.navigate("child/$newArg") {
popUpTo("parent/$newArg") {
saveState = true
}
}
}) {
Text(text = arg)
}
}
What you may want to do is launch the parent composable as single top, and when navigating from child composable, you can send the new argument and launch it as single top again. Here is how you can achieve if you are using compose navigation;
You can launch ParentScreen for the first time like this;
navController.navigate("parent/first") {
launchSingleTop = true
}
Then navigate to ChildScreen as usual;
navController.navigate("child/firstSecond")
In ChildScreen navigate to ParentSceen like this, this will remove the ChildScreen from the nav stack and launch the ParentScreen as single top, resulting only ParentScreen in the stack with new parameter;
navController.navigate("parent/$newArg") {
launchSingleTop = true
popUpTo("child/{someArg}") {
inclusive = true
}
}
Full code here;
#Composable
fun SampleNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
onBackClick: () -> Unit,
startDestination: String = "start_dest",
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("start_dest") {
Button(onClick = {
navController.navigate("parent/first") {
launchSingleTop = true
}
}) {
Text(text = "move to parent")
}
}
composable("parent/{someArg}") {
val arg = it.arguments?.getString("someArg").orEmpty()
ParentScreen(
title = arg,
navStackSize = navController.backQueue.size.toString(),
onButtonClick = {
navController.navigate("child/firstSecond")
}
)
}
composable("child/{someArg}") {
val arg = it.arguments?.getString("someArg").orEmpty()
val newArg = arg + "second"
ChildScreen(
title = arg,
onButtonClick = {
navController.navigate("parent/$newArg") {
launchSingleTop = true
// this will navigate current composable from stack when navigating
popUpTo("child/{someArg}") {
inclusive = true
}
}
}
)
}
}
}
#Composable
fun ParentScreen(
title: String,
navStackSize: String,
onButtonClick: () -> Unit,
) {
Column {
Text(text = "Parent Screen, stack size: $navStackSize")
Button(onClick = onButtonClick) {
Text(text = title)
}
}
}
#Composable
fun ChildScreen(
title: String,
onButtonClick: (String) -> Unit,
) {
Column {
Text(text = "Child Screen")
Button(onClick = { onButtonClick(title) }) {
Text(text = title)
}
}
}

Jetpack Compose - Using Nested NavGraph with BottomNavBar and make Icons selected

I want to implement a BottomNavBar with 3 items.
Each navigating to a new navGraph. (Nested Navigation)
Home = "HomeGraph"
Explore = "ExploreGraph"
Profile = "ProfileGraph"
HomeGraph:
#OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.addHomeGraph(navController: NavHostController) {
navigation(
startDestination = "feed",
route = "HomeGraph"
) {
composable(
route = "feed"
) {
}
composable(
route = "comments"
) {
}
}
}
And my BottomNavBar navigates between those 3 graphs:
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
BottomNavigationItem.HomeGraph,
BottomNavigationItem.ExploreGraph,
BottomNavigationItem.ProfileGraph
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
BottomNavigation() {
Row(horizontalArrangement = Arrangement.Center) {
items.forEach { item ->
BottomNavigationItem(
icon = {
if (currentRoute == item.route) {
Icon(
painterResource(id = item.iconPressed),
contentDescription = item.title
)
} else {
Icon(
painterResource(id = item.iconNormal),
contentDescription = item.title
)
}
},
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.onSurface,
alwaysShowLabel = false,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
}
Now the problem is that my "Home" BottomNav Icon is not changing cause "selected" is never true.
selected = currentRoute == item.route,
How can I check if I am still on the same navigationGraph?
So when I go down the "HomeGraph" for example to "feed" or "comments" it should still indicate the "Home" Icon on my BottomNavBar as selected.
"feed" and "Comments" have in common, that their parent NavGraph is called "HomeGraph", how can I check for that?
With other words: I want pretty much the same BottomNavBar behaviour as Instagram
Instead of matching navBackStackEntry?.destination?.route, try looking for your route in the NavDestination's hierarchy.
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Then in your forEach:
val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true

Problem with LaunchedEffect in composables of HorizontalPager

I'm creating a project with Compose, but I ran into a situation that I couldn't solve.
View Model:
data class OneState(
val name: String = "",
val city: String = ""
)
sealed class OneChannel {
object FirstStepToSecondStep : OneChannel()
object Finish : OneChannel()
}
#HiltViewModel
class OneViewModel #Inject constructor() : ViewModel() {
private val viewModelState = MutableStateFlow(OneState())
val screenState = viewModelState.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = viewModelState.value
)
private val _channel = Channel<OneChannel>()
val channel = _channel.receiveAsFlow()
fun changeName(value: String) {
viewModelState.update { it.copy(name = value) }
}
fun changeCity(value: String) {
viewModelState.update { it.copy(city = value) }
}
fun firstStepToSecondStep() {
Log.d("OneViewModel", "start of method first step to second step")
if (viewModelState.value.name.isBlank()) {
Log.d("OneViewModel", "name is empty, nothing should be done")
return
}
Log.d(
"OneViewModel",
"name is not empty, first step to second step event will be send for composable"
)
viewModelScope.launch {
_channel.send(OneChannel.FirstStepToSecondStep)
}
}
fun finish() {
Log.d("OneViewModel", "start of method finish")
if (viewModelState.value.city.isBlank()) {
Log.d("OneViewModel", "city is empty, nothing should be done")
return
}
Log.d(
"OneViewModel",
"city is not empty, finish event will be send for composable"
)
viewModelScope.launch {
_channel.send(OneChannel.Finish)
}
}
}
This ViewModel has a MutableStateFlow, a StateFlow to be collected on composable screens and a Channel/Flow for "one time events".
The first two methods are to change a respective state and the last two methods are to validate some logic and then send an event through the Channel.
Composables:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun FirstStep(
viewModel: OneViewModel,
nextStep: () -> Unit
) {
val state by viewModel.screenState.collectAsState()
LaunchedEffect(key1 = Unit) {
Log.d("FirstStep (Composable)", "start of launched effect block")
viewModel.channel.collect { channel ->
when (channel) {
OneChannel.FirstStepToSecondStep -> {
Log.d("FirstStep (Composable)", "first step to second step action")
nextStep()
}
else -> Log.d(
"FirstStep (Composable)",
"another action that should be ignored in this scope"
)
}
}
}
Column(modifier = Modifier.fillMaxSize()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
value = state.name,
onValueChange = { viewModel.changeName(value = it) },
placeholder = { Text(text = "Type our name") }
)
Spacer(modifier = Modifier.weight(weight = 1F))
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = { viewModel.firstStepToSecondStep() }
) {
Text(text = "Next Step")
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun SecondStep(
viewModel: OneViewModel,
prevStep: () -> Unit,
finish: () -> Unit
) {
val state by viewModel.screenState.collectAsState()
LaunchedEffect(key1 = Unit) {
Log.d("SecondStep (Composable)", "start of launched effect block")
viewModel.channel.collect { channel ->
when (channel) {
OneChannel.Finish -> {
Log.d("SecondStep (Composable)", "finish action //todo")
finish()
}
else -> Log.d(
"SecondStep (Composable)",
"another action that should be ignored in this scope"
)
}
}
}
Column(modifier = Modifier.fillMaxSize()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
value = state.city,
onValueChange = { viewModel.changeCity(value = it) },
placeholder = { Text(text = "Type our city name") }
)
Spacer(modifier = Modifier.weight(weight = 1F))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Button(
modifier = Modifier.weight(weight = 1F),
onClick = prevStep
) {
Text(text = "Previous Step")
}
Button(
modifier = Modifier.weight(weight = 1F),
onClick = { viewModel.finish() }
) {
Text(text = "Finish")
}
}
}
}
#OptIn(ExperimentalPagerApi::class)
#Composable
fun OneScreen(viewModel: OneViewModel = hiltViewModel()) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage = 0)
val pages = listOf<#Composable () -> Unit>(
{
FirstStep(
viewModel = viewModel,
nextStep = {
coroutineScope.launch {
pagerState.animateScrollToPage(page = pagerState.currentPage + 1)
}
}
)
},
{
SecondStep(
viewModel = viewModel,
prevStep = {
coroutineScope.launch {
pagerState.animateScrollToPage(page = pagerState.currentPage - 1)
}
},
finish = {}
)
}
)
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 1F),
state = pagerState,
count = pages.size,
userScrollEnabled = false
) { index ->
pages[index]()
}
HorizontalPagerIndicator(
modifier = Modifier
.padding(vertical = 16.dp)
.align(alignment = Alignment.CenterHorizontally),
pagerState = pagerState,
activeColor = MaterialTheme.colorScheme.primary
)
}
}
OneScreen has a HorizontalPager (from the Accompanist library) which receives two other composables, FirstStep and SecondStep, these two composables have their own LaunchedEffect to collect any possible event coming from the View Model.
Dependencies used:
implementation 'androidx.navigation:navigation-compose:2.5.2'
implementation 'com.google.dagger:hilt-android:2.43.2'
kapt 'com.google.dagger:hilt-android-compiler:2.43.2'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
implementation 'com.google.accompanist:accompanist-pager:0.25.1'
implementation 'com.google.accompanist:accompanist-pager-indicators:0.25.1'
The problem:
After typing something in the name field and clicking to go to the next step, the flow happens normally. When clicking to go back to the previous step, it also works normally. But now when clicking to go to the next step again, the collect in the LaunchedEffect of the FirstStep is not collected, instead the collect in LaunchedEffect of the SecondStep is, resulting in no action, and if click again, then collect in FirstStep works.
Some images that follow the logcat:
when opening the app
after typing something and clicking to go to the next step
going back to the first step
clicking to go to next step (problem)
clicking for the second time (works)
The problem is that HorizontalPager creates both the current page and the next page. When current page is FirstStep, both collectors are active and will be triggered sequentially.
Let's look at the three jump attempts on the first page. The first attempt is received by collector in FirstStep and successfully jumps to the second page. The second attempt is received by collector in SecondStep and fails. The third attempt succeeds again.
Actually, HorizontalPager is LazyRow, so this should be the result of LazyLayout's place logic.
To solve this problem, I suggest merging the two LaunchedEffect and moving it into OneScreen. In fact, the viewmodel should all be moved to the top of the OneScreen, for cleaner code.
At last, here is my simplified code if you want try it.
#Composable
fun Step(index: Int, flow: Flow<String>, onSwitch: () -> Unit, onSend: () -> Unit) {
LaunchedEffect(Unit) {
println("LaunchEffect$index")
flow.collect { println("Step$index:$it") }
}
Column {
Text(text = index.toString(), style = MaterialTheme.typography.h3)
Button(onClick = onSwitch) { Text(text = "Switch Page") }
Button(onClick = onSend) { Text(text = "Send") }
}
}
#OptIn(ExperimentalPagerApi::class)
#Composable
fun Test() {
val channel = remember { Channel<String>() }
val flow = remember { channel.receiveAsFlow() }
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState()
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
count = 4,
userScrollEnabled = false,
) { index ->
Step(index = index, flow = flow,
onSwitch = {
scope.launch { pagerState.scrollToPage((index + 1) % pagerState.pageCount) }
},
onSend = {
scope.launch { channel.send("Test") }
}
)
}
}
If you keep click send button at first page, it will print:

Jetpack Compose TopAppBar with dynamic actions

#Composable
fun TopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable (() -> Unit)? = null,
actions: #Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
)
actions: #Composable RowScope.() -> Unit = {}
Usage Scenario:
Using Compose Navigation to switch to different "screens", so the TopAppBar actions will be changed accordingly. Eg. Share buttons for content screen, Filter button for listing screen
Tried passing as a state to the TopAppBar's actions parameter, but having trouble to save the lambda block for the remember function.
val (actions, setActions) = rememberSaveable { mutableStateOf( appBarActions ) }
Want to change the app bar actions content dynamically. Any way to do it?
This the approach I used but I'm pretty new on compose, so I cannot be sure it is the correct approach.
Let's assume I have 2 screens: ScreenA and ScreenB
They are handled by MainActivity screen.
This is our MainActivity:
#ExperimentalComposeUiApi
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CoolDrinksTheme {
val navController = rememberNavController()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var appBarState by remember {
mutableStateOf(AppBarState())
}
Scaffold(
topBar = {
SmallTopAppBar(
title = {
Text(text = appBarState.title)
},
actions = {
appBarState.actions?.invoke(this)
}
)
}
) { values ->
NavHost(
navController = navController,
startDestination = "screen_a",
modifier = Modifier.padding(
values
)
) {
composable("screen_a") {
ScreenA(
onComposing = {
appBarState = it
},
navController = navController
)
}
composable("screen_b") {
ScreenB(
onComposing = {
appBarState = it
},
navController = navController
)
}
}
}
}
}
}
}
}
As you can see I'm using a mutable state of a class which represents the state of our MainActivity (where the TopAppBar is declared and composed), in this example there is the title and the actions of our TopAppBar.
This mutable state is set with a callback function called inside the composition of each screen.
Here you can see the ScreenA
#Composable
fun ScreenA(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen A",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Filter,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen A"
)
Button(onClick = {
navController.navigate("screen_b")
}) {
Text(text = "Navigate to Screen B")
}
}
}
And the ScreenB
#Composable
fun ScreenB(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen B",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen B"
)
Button(onClick = {
navController.popBackStack()
}) {
Text(text = "Navigate back to Screen A")
}
}
}
And finally this is the data class of our state:
data class AppBarState(
val title: String = "",
val actions: (#Composable RowScope.() -> Unit)? = null
)
In this way you have a dynamic appbar declared in the main activity but each screen is responsable to handle the content of the appbar.
First you need to add navigation dependency on you jetpack compose projects.
You can read the doc from this https://developer.android.com/jetpack/compose/navigation
def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-compose:$nav_version"
Then define your screen in sealed class:
sealed class Screen(var icon: ImageVector, var route: String) {
object ContentScreen: Screen(Icons.Default.Home, "home")
object ListingScreen: Screen(Icons.Default.List, "list")
}
and this is the navigation function look like
#Composable
fun Navigation(paddingValues: PaddingValues, navController: NavHostController) {
NavHost(navController, startDestination = Screen.ContentScreen.route, modifier = Modifier.padding(paddingValues)) {
composable(Screen.ContentScreen.route) {
//your screen content
}
composable(Screen.ListingScreen.route) {
//your listing screen here
}
}
}
Finally in your mainactivity class
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestAppTheme {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "main screen") }, actions = {
if (currentRoute == Screen.ContentScreen.route) {
//your share button action here
} else if (currentRoute == Screen.ListingScreen.route) {
//your filter button here
} else {
//other action
}
})
}
) {
Navigation(paddingValues = it, navController = navController)
}
}
}
}
I'm so sorry if the explanation to sort, because the limitation of my English

Jetpack compose navigation closes app instead of returning to previous screen

I have implemented the accompanist navigation animation library in my project and have stumbled into two issues. The first issue is that the animations aren't being applied when navigating from one screen to another. The second issue is that the "back" of the system closes the app to the background instead of returning to the previous screen.
Here is the layout of the app starting from the MainActivity.
MainActivity.kt
#ExperimentalAnimationApi
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val preDrawListener = ViewTreeObserver.OnPreDrawListener { false }
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.MyHomeTheme)
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(preDrawListener)
lifecycleScope.launch {
setContent {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = true
)
systemUiController.setNavigationBarColor(
color = Color(0x40000000),
darkIcons = false
)
}
MyHomeApp(
currentRoute = Destinations.Welcome.WELCOME_ROUTE
)
}
unblockDrawing()
}
}
private fun unblockDrawing() {
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
content.viewTreeObserver.addOnPreDrawListener { true }
}
}
MyHomeApp.kt
#ExperimentalAnimationApi
#Composable
fun MyHomeApp(currentRoute: String) {
MyHomeTheme {
ProvideWindowInsets {
val navController = rememberAnimatedNavController()
val scaffoldState = rememberScaffoldState()
val darkTheme = isSystemInDarkTheme()
val items = listOf(
HomeTab.Dashboard,
HomeTab.Details,
HomeTab.Settings
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val bottomPaddingModifier = if (currentDestination?.route?.contains("welcome") == true) {
Modifier
} else {
Modifier.navigationBarsPadding()
}
Scaffold(
modifier = Modifier
.fillMaxSize()
.then(bottomPaddingModifier),
scaffoldState = scaffoldState,
bottomBar = {
if (currentDestination?.route in items.map { it.route }) {
BottomNavigation {
items.forEach { screen ->
BottomNavigationItem(
label = { Text(screen.title) },
icon = {},
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
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
}
}
)
}
}
}
}
) { innerPadding ->
MyHomeNavGraph(
modifier = Modifier.padding(innerPadding),
navController = navController,
startDestination = navBackStackEntry?.destination?.route ?: currentRoute
)
}
}
}
}
sealed class HomeTab(
val route: String,
val title: String
) {
object Dashboard : HomeTab(
route = Destinations.Home.HOME_DASHBOARD,
title = "Dashboard"
)
object Details : HomeTab(
route = Destinations.Home.HOME_DETAILS,
title = "Details"
)
object Settings : HomeTab(
route = Destinations.Home.HOME_SETTINGS,
title = "Settings"
)
}
MyHomeNavGraph.kt
#ExperimentalAnimationApi
#Composable
fun MyHomeNavGraph(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String
) {
val actions = remember(navController) { Actions(navController = navController) }
AnimatedNavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(
route = Destinations.Welcome.WELCOME_ROUTE,
enterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) {
WelcomeScreen(
navigateToLogin = actions.navigateToWelcomeLogin,
navigateToRegister = actions.navigateToWelcomeRegister,
)
}
composable(
route = Destinations.Welcome.WELCOME_LOGIN_ROUTE,
enterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) {
WelcomeLoginScreen(
// Arguments will be passed to navigate to the home screen or other
)
}
}
}
class Actions(val navController: NavHostController) {
// Welcome
val navigateToWelcome = {
navController.navigate(Destinations.Welcome.WELCOME_ROUTE)
}
val navigateToWelcomeLogin = {
navController.navigate(Destinations.Welcome.WELCOME_LOGIN_ROUTE)
}
}
For simplicity's sake, you can assume that the screens are juste a box with a button in the middle which executes the navigation when they are clicked.
The accompanist version I am using is 0.24.1-alpha (the latest as of this question) and I am using compose version 1.2.0-alpha02 and kotlin 1.6.10.
In terms of animation, the only difference I can see with the accompanist samples is that I don't pass the navController to the screens but I don't see how that could be an issue.
And in terms of using the system back which should return to a previous, I'm genuinely stuck in terms of what could cause the navigation to close the app instead of going back. On other projects, the system back works just fine but not with this one. Is the use of the accompanist navigation incompatible ? I'm not sure.
Any help is appreciated!
I found the source of the issue.
The fact that I was setting the startDestination parameter to navBackStackEntry?.destination?.route ?: currentRoute meant that each change to the navBackStackEntry recomposed the MyHomeNavGraph and hence the backstack was reset upon the recomposition.
Note to self, watch out when copying navigation from multiple sources!

Categories

Resources