Conditional navigation start destination in jetpack compose - android

I am using navigation-compose along with bottom bar in jetpack compose. I want to show different bottom bar for different type of user. For that, I need to set conditional startDestination in NavHost. How do i do that?
I've tried below but it change startDestination but does not reflect in UI.
val user by remember { mutableStateOf(viewModel.user) }.collectAsState()
var startDestination = Screens.UserType1.route
LaunchedEffect(user) {
startDestination = if (loggedInUser?.userType == UserType.Seller) Screens.SellerHome.route else Screens.BuyerHome.route
}
While below code throws java.util.NoSuchElementException: List contains no element matching the predicate.
when (user?.userType) {
UserType.Seller -> {
startDestination = Screens.SellerHome.route
}
UserType.Buyer -> {
startDestination = Screens.BuyerHome.route
}
else -> {}
}
My NavHost
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
...
}

Change this
var startDestination = Screens.UserType1.route
To this
var startDestination by remember { mutableStateOf(Screens.UserType1.route) }

Related

How to persist ViewModel when navigating in Android?

new to android, and doing it the Compose way.
I have a BottomNavigation and three taps, each draw a different screen.
#Composable
fun SetupNavigation(navHostController: NavHostController) {
NavHost(navController = navHostController, startDestination = "home") {
composable(route = "first") {
FirstScreen()
}
composable(route = "second") {
SecondScreen()
}
composable(route = "third") {
ThirdScreen()
}
}
}
#Composable
fun FirstScreen(
firstScreenViewModel: FirstScreenViewModel = hiltViewModel()
) {
val uiState by firstScreenViewModel.uiState.collectAsState()
Log.i("FirstScreen", "uiState: $uiState")
val coins = uiState.coinsList
ItemsList(items = items)
}
Using the backbutton or just tapping through each viewModel of the different screens seems to reinit. Is that the expected behavior? I like to persist the viewModel when switching routes.
I don't have fragments, just one activity with composable
TIA
If you want to share viewModel instance across your Nav Graph and your host activity you can do 2 things
one is assigning the local ViewModelStoreOwner
#Composable
fun SetupNavigation(navHostController: NavHostController) {
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
NavHost(navController = navHostController, startDestination = "home") {
composable(route = "first") {
FirstScreen(firstScreenViewModel = hiltViewModel(viewModelStoreOwner))
}
composable(route = "second") {
SecondScreen() // you can also do it here if you want
}
composable(route = "third") {
ThirdScreen() // you can also do it here if you want
}
}
}
or another approach is creating a Local composition of your Activity instance and set it up like this

Setting up a compose scaffold with NavHost at higher level

I am trying to setup an application flow in which there is a main route/screen, followed by a home route/screen where the home screen contains a scaffold to setup bottom bar navigation.
I originally had the scaffold setup at the main (top level) route where the scaffold content was just the NavHost ie:
#Composable
fun MainScreen() {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
Scaffold(
scaffoldState = scaffoldState,
drawerGesturesEnabled = false,
drawerContent = {...},
bottomBar = {...}
) {
NavHost(
navController = navController,
startDestination = "tab1"
) {
tab1Graph(navController)
tab2Graph(navController)
tab3Graph(navController)
}
}
}
}
Which is fine I suppose, however since only my home route needs a scaffold, why have the scaffold at the top level instead of at the lower level in which its needed.
Here is my attempt to move the scaffold into the home screen:
fun NavGraphBuilder.homeGraph(
navController: NavController,
bottomSheetNavigator: BottomSheetNavigator
) {
composable("home") {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
ModalBottomSheetLayout(bottomSheetNavigator) {
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {...},
bottomBar = {...}
) {
// Not entirely sure how to setup bottom nav tabs within the scaffold?
}
}
}
}
However I am lost at how to get the tab content to live inside the scaffold based on route. EG the same magic that happens when you embed the NavHost inside the scaffold.
I'm currently working on a project where I solved the same problem.
First, in the MainActivity I call my MainNavGraph, then in the main NavGraph, I call my HomeScreen Composable which contains the BottomNavGraph and the screens to display in this HomeScreen. Finally, in the BottomNavGraph I include everything related to the HomeScreen
MainActivity :
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
}
val yourViewModel = viewModel(modelClass = YourViewModel::class.java)
YourProjectTheme {
MainNavGraph(yourViewModel)
}
}
}
Main navigation
#Composable
fun MainNavGraph(
yourMainViewModel: YourMainViewModel?,
){
val navController = rememberNavController()
NavHost(
navController = NavController,
startDestination = "top_level_composable"
){
composable("top_level_composable"){
TopLevelComposable{
navController.navigate("home_screen")
}
}
composable("home_screen"){
home()
}
}
}
Home Screen
#Composable
fun HomeScreen(){
val homeNavController = rememberNavController()
val anotherViewModel = viewModel(modelClass = AnotherViewModel::class.java)
Scaffold(
...
...
bottomBar = { BottomNavigationBar(navController) }
content = { padding ->
Box(modifier = Modifier.padding(padding)){
HomeNavGraph(
navController = homeNavController,
anotherViewModel = anotherViewModel
)
}
}
)
}
HomeNavGraph
#Composable
fun HomeNavGraph(
navController: NavHostController,
anotherViewModel: AnotherViewModel
) {
NavHost(
navController = navController,
route = "home_nav",
startDestination = "welcome"
){
composable("welcome"){
WelcomeScreen(navController)
}
composable("posts"){
PostsScreen(navController, anotherViewModel)
}
composable("search"){
SearchScreen(navController)
}
composable("messages"){
MessagesScreen(navController)
}
composable("profile"){
ProfileScreen(navController)
}
}
}
BottomNavigation
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
"welcome",
"posts",
"search",
"messages",
"profile",
)
BottomNavigation(
backgroundColor = Color.White,
contentColor = Color.Black,
modifier = Modifier.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)),
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
label = { Text(text = item) },
selectedContentColor = GWpalette.ImperialRed,
unselectedContentColor = GWpalette.Gunmetal,
alwaysShowLabel = false,
selected = currentRoute == item,
onClick = {
navController.navigate(item) {
// 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
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
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
}
}
)
}
}
}
You can see how to create Bottom Navigation Bar with Jetpack Compose here
https://johncodeos.com/how-to-create-bottom-navigation-bar-with-jetpack-compose/
You can conditionally use the Scaffold based on the current route:
val navController = rememberNavController()
val navBackStateEntry by navController.currentBackStackEntryAsState()
if (navBackStateEntry?.destination?.route == "my_route") {
Scaffold(...)
} else {
Text("No scaffold")
}

Jetpack Component screens overlaps, or partially rendered, when being navigated with Jetpack Compose navigation controller

Here is the symptom - Youtube
It looks like screens are partially rendered, or overlapped, when being navigated with Jetpack Compose navigation controller.
I try to find an answer one, but no one seems to meet this problem at all.
Here are related code snippets
#Composable
fun NavHostAuth(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = NavRoutesAuth.SignIn.route
) {
composable(NavRoutesAuth.SignIn.route) {
EnterAnimation {
ScreenSignInWithInputValidationDebounce(navController)
}
}
composable(NavRoutesAuth.SignUp.route) {
EnterAnimation {
ScreenSignUpWithInputValidationDebounce(navController)
}
}
}
}
enum class NavRoutesAuth(val route: String) {
SignIn("auth/signin"),
SignUp("auth/signup"),
}
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun EnterAnimation(content: #Composable () -> Unit) {
val visible by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = visible,
modifier = Modifier,
enter = slideInVertically(initialOffsetY = { -40 })
+ expandVertically(expandFrom = Alignment.Top)
+ fadeIn(initialAlpha = 0.3f),
exit = slideOutVertically()
+ shrinkVertically()
+ fadeOut()
) {
content()
}
}
What might be the cause?
Thanks.

Jetpack Compose TopAppBar flickers after setting BottomNavView with Navigation component

Working on a Jetpack Compose app with a BottomNavigationView and Navigation component, I did the usual setup for the bottom bar inside the activity:
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
navigationView.setupWithNavController(navController)
In each screen I am using a composable TopAppBar.
Unfortunately, after adding the setupWithNavController call, the TopAppBar started flickering in all the navigation bar destinations (but not in deeper destinations).
How can I fix the flickering?
I got the same issue. What helped, to my surprise, was lowering androidx.navigation:navigation-compose version to 2.4.0-alpha04.
I cannot answer you exactly how to fix the flickering, but I can tell you that the recommended way to work with a Navigation component, TopBars and BottomBars in Jetpack Compose, is to use the Scaffold component and the navigation-compose component.
Compose Navigation component dependency:
// Navigation
implementation "androidx.navigation:navigation-compose:1.0.0-alpha09"
It is still in alpha, but so far it has been working really well. Just try to keep posted about all updates.
Example usage:
#Composable
fun App() {
val navController = rememberNavController()
Scaffold(
topBar = {
TopAppBar {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(text = "App Title")
}
}
},
bottomBar = {
BottomAppBar {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = { navController.navigate("home") }) {
Text(text = "Home")
}
TextButton(onClick = { navController.navigate("favorites") }) {
Text(text = "Favorites")
}
TextButton(onClick = { navController.navigate("profile") }) {
Text(text = "Profile")
}
}
}
}
) {
NavHost(navController, startDestination = "home") {
composable("home") {
Home()
}
composable("favorites") {
Favorites()
}
composable("profile") {
Profile()
}
}
}
}

Animating between Composables in Navigation with Compose

I have started trying out Navigation for compose.
I created my 2 Composables and everything is working fine.
But what I'm missing is Animations (or Transitions) between the pages. I didn't find any resources pointing out how to do it in Compose.
I know all animations are based on states in Compose, but the only thing I know is the Navigation Back Stack.
You can use the composable I made to show enter animation (configure preferable effects in "enter" and "exit" parameters)
fun EnterAnimation(content: #Composable () -> Unit) {
AnimatedVisibility(
visible = true,
enter = slideInVertically(
initialOffsetY = { -40 }
) + expandVertically(
expandFrom = Alignment.Top
) + fadeIn(initialAlpha = 0.3f),
exit = slideOutVertically() + shrinkVertically() + fadeOut(),
content = content,
initiallyVisible = false
)
}
You can use it like this:
NavHost(
navController = navController,
startDestination = "dest1"
) {
composable("dest1") {
EnterAnimation {
FirstScreen(navController)
}
}
composable("dest2") {
EnterAnimation {
SecondScreen(navController)
}
}
}
Accompanist version 0.16.1 and above supports animation between destinations. Here is the article for more info.
implementation("com.google.accompanist:accompanist-navigation-animation:0.16.1")
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
val navController = rememberAnimatedNavController()
AnimatedNavHost(navController, startDestination = "first") {
composable(
route = "first",
enterTransition = { _, _ -> slideInHorizontally(animationSpec = tween(500)) },
exitTransition = { _, _ -> slideOutHorizontally(animationSpec = tween(500)) }
) {
FirstScreen()
}
composable(
route = "second",
enterTransition = { _, _ -> slideInHorizontally(initialOffsetX = { it / 2 }, animationSpec = tween(500)) },
exitTransition = { _, _ -> slideOutHorizontally(targetOffsetX = { it / 2 }, animationSpec = tween(500)) }
) {
SecondScreen()
}
}
Result:
In alpha-09 this is not supported. :(
Please, star this issue: https://issuetracker.google.com/issues/172112072
Due to yesterday's update (version 2.4.0-alpha05):
Navigation Compose’s NavHost now always uses Crossfades when navigating through destinations.
Lately Google Accompanist has added a library which provides Compose Animation support for Jetpack Navigation Compose.. Do check it out. 👍🏻
https://github.com/google/accompanist/tree/main/navigation-animation
You can use the library I did. It provides easy navigation and is written using AnimatedVisibility so you can use compose transitions to animate any screen state (enter, exit)
https://github.com/vldi01/AndroidComposeRouting

Categories

Resources