How to persist ViewModel when navigating in Android? - 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

Related

How to contribute to AppBar from Screen in jetpack compose

I want to implement a simple user flow, where the user sees multiple screens to input data. The flow should share a common navbar where each screen can contribute its menu items to when it is active (e.g. add a "search" or a "next" button). The navbar also has buttons belonging conceptually to the user flow and not to individual screens (like the back button and a close button). Screens should be reusable in other contexts, so screens should not know about the flow they operate in.
Technically the user flow is implemented as a compose function defining the navbar and using compose navigation. Each screen is implemented as a separate compose function.
In fragment/view based Android this scenario was supported out of box with onCreateOptionsMenu and related functions. But how would I do this in compose? I could not find any guidance on that topic.
To illustrate the problem in code:
#Composable
fun PaymentCoordinator(
navController: NavHostController = rememberNavController()
) {
AppTheme {
Scaffold(
bottomBar = {
BottomAppBar(backgroundColor = Color.Red) {
IconButton(onClick = navController::popBackStack) {
Icon(Icons.Filled.ArrowBack, "Back")
}
Spacer(modifier = Modifier.weight(1f))
// 0..n IconButtons provided by the active Screen
// should be inserted here
// How can we do that, because state should never
// go up from child to parent
// this button (or at least its text and onClick action) should
// be defined by the currently visible Screen as well
Button(
onClick = { /* How to call function of screen? */ }
) {
Text("Next"))
}
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
NavHost(
navController = navController,
startDestination = "selectAccount"
) {
// screens that can contribute items to the menu
composable("selectAccount") {
AccountSelectionRoute(
onAccountSelected = {
navController.navigate("nextScreen")
}
)
}
composable("...") {
// ...
}
}
}
}
}
}
I came up with an approach leveraging side effects and lifecycle listener to achieve my goal. Basically whenever a screen becomes active (ON_START) it informs the parent (coordinator) about its menu configuration. The coordinator evaluates the configuration and updates the navbar accordingly.
The approach is based on Googles documentation on side effects (https://developer.android.com/jetpack/compose/side-effects#disposableeffect)
The approach feels complicated and awkward and I think the compose framework is missing some functionality to achieve this here. However, my implementation seems to be working fine in my test use case.
Helper classes
// currently I only need to configure a single button, however the approach
// can be easily extended now (you can put anything inside rightButton)
data class MenuConfiguration(
val rightButton: #Composable () -> Unit
)
#Composable
fun SimpleMenuConfiguration(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
onUnregisterMenuConfiguration: () -> Unit,
rightButton: #Composable () -> Unit
) {
val currentOnRegisterMenuConfiguration by rememberUpdatedState(onRegisterMenuConfiguration)
val currentOnUnregisterMenuConfiguration by rememberUpdatedState(onUnregisterMenuConfiguration)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnRegisterMenuConfiguration(
MenuConfiguration(
rightButton = rightButton
)
)
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnUnregisterMenuConfiguration()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
Coordinator level
#Composable
fun PaymentCoordinator(
navController: NavHostController = rememberNavController()
) {
var menuConfiguration by remember { mutableStateOf<MenuConfiguration?>(null) }
AppTheme {
Scaffold(
bottomBar = {
BottomAppBar(backgroundColor = Color.Red) {
IconButton(onClick = navController::popBackStack) {
Icon(Icons.Filled.ArrowBack, "Back")
}
Spacer(modifier = Modifier.weight(1f))
menuConfiguration?.rightButton?.invoke()
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
PaymentNavHost(
navController = navController,
finishedHandler = finishedHandler,
onRegisterMenuConfiguration = { menuConfiguration = it },
onUnregisterMenuConfiguration = { menuConfiguration = null }
)
}
}
}
}
#Composable
fun PaymentNavHost(
navController: NavHostController = rememberNavController(),
onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
onUnregisterMenuConfiguration:() -> Unit
) {
NavHost(
navController = navController,
startDestination = "selectAccount"
) {
composable("selectAccount") {
DemoAccountSelectionRoute(
onAccountSelected = {
navController.navigate("amountInput")
},
onRegisterMenuConfiguration = onRegisterMenuConfiguration,
onUnregisterMenuConfiguration = onUnregisterMenuConfiguration
)
}
composable("amountInput") {
AmountInputRoute(
onRegisterMenuConfiguration = onRegisterMenuConfiguration,
onUnregisterMenuConfiguration = onUnregisterMenuConfiguration,
onFinished = {
...
}
)
}
}
}
Screen level
#Composable
internal fun AmountInputRoute(
onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
onUnregisterMenuConfiguration:() -> Unit,
onFinished: (Amount?) -> Unit
) {
SimpleMenuConfiguration(
onRegisterMenuConfiguration = onRegisterMenuConfiguration,
onUnregisterMenuConfiguration = onUnregisterMenuConfiguration,
rightButton = {
Button(
onClick = {
...
}
) {
Text(text = stringResource(id = R.string.next))
}
}
)

Android Jetpack Compose Navigation doesn't work

I'm making an android app using Jetpack Compose and I have a problem. I used navigation to change the screen. I wanted to change firstRunLayout to loginLayout. You can see that i put that log.d code in order to check if this worked and I found out that logcat prints that successfully. I want to change the screen when I navigate. Is there something wrong with my code?
#Composable
fun setUpNavHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.FirstRun.route) {
composable(route = Screen.FirstRun.route) {
firstRunLayout(navController)
Log.d("NavHost", "FirstRun")
}
composable(route = Screen.Login.route) {
loginLayout()
Log.d("NavHost", "Login")
}
}
}
p.s. I checked that there are Composable annotations at those layouts.
Button(onClick = {
Log.d("FirstRunActivity", "Button clicked")
navController.navigate(route = Screen.Login.route)
}, colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFFEF4746)), modifier = Modifier
.height(40.dp)
.width(300.dp), shape = RoundedCornerShape(50)) {
Text(text = "Continue", fontSize = 15.sp, color = Color.White)
}
I used the button to navigate. You can see navController.navigate above. Also, this is the way that this navController was declared.
class FirstRunActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MemorizableTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFFEF4746)
) {
val navController = rememberNavController()
setUpNavHost(navController = navController)
firstRunLayout(navController)
}
}
}
}
}
The problem is in your Activity, you call the navHost, then you call the firstRunLayout screen, so the firstRunLayout screen will be on the top of the navHost, you need to delete the call of firstRunLayout in your activity, so your code should look like this:
class FirstRunActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MemorizableTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFFEF4746)
) {
val navController = rememberNavController()
setUpNavHost(navController = navController)
}
}
}
}
}

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 Compose - Bottom navigation bar isn't showing with nested navGraph

I'm developing an android application using Jetpack Compose. For now, I have several screens (Login, Register, Messages, Profile and Settings). I used a root NavGraph to host the navGraphs of authentication and main screen (See the codes below).
RootNavGraph.kt
#Composable
fun RootNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = AUTHENTICATION_ROUTE,
route = ROOT_ROUTE
) {
authNavGraph(navController = navController)
bottomNavGraph(navController = navController)
}
}
authNavGraph.kt
fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation(
startDestination = AuthenticationScreens.Login.route,
route = AUTHENTICATION_ROUTE
) {
composable(route = AuthenticationScreens.Login.route) {
LoginPage(navController = navController)
}
composable(route = AuthenticationScreens.Register.route) {
RegisterPage(navController = navController)
}
}
}
bottomNavGraph.kt
fun NavGraphBuilder.bottomNavGraph(navController: NavHostController) {
navigation(
startDestination = BottomBarScreen.Messages.route,
route = BOTTOM_ROOT_ROUTE
) {
composable(route = BottomBarScreen.Messages.route) {
Messages()
}
composable(route = BottomBarScreen.Profile.route) {
Profile(navController = navController)
}
composable(route = BottomBarScreen.Settings.route) {
Settings()
}
}
}
I have the MainScreen that contains the bottom navigation bar.
#Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomBar(navController = navController)
}
) {
//bottomNavGraph(navController)
}
}
#Composable
fun BottomBar(navController: NavHostController) {
val screens = listOf(
BottomBarScreen.Messages,
BottomBarScreen.Profile,
BottomBarScreen.Settings
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigation() {
screens.forEach { screen ->
AddItem(
screen = screen,
currentDestination = currentDestination,
navController = navController
)
}
}
}
#Composable
fun RowScope.AddItem(
screen: BottomBarScreen,
currentDestination: NavDestination?,
navController: NavHostController
) {
BottomNavigationItem(
label = {
Text(text = screen.title)
},
alwaysShowLabel = false,
icon = {
Icon(imageVector = screen.icon, contentDescription = "Navigation Icon")
},
selected = currentDestination?.hierarchy?.any {
it.route == screen.route
} == true,
unselectedContentColor = LocalContentColor.current.copy(alpha = ContentAlpha.disabled),
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
}
My problem is: after switching to use the nested nav graphs, I cannot use the bottomNavGraph directly in the MainScreen since it's no longer a NavHost and thus the bottom navigation bar isn't showing. I would appreciate if any one can help me with that.
For bottom navigation use the NavHost to show the bottom bar
fun HomeNavGraph(navHostController: NavHostController) {
NavHost(
navController = navHostController,
route = Graph.Home,
startDestination = BottomBar.Home.route
) {
composable(BottomBar.Home.route){
HomeScreenContent()
}
then use this graph in your homescreen scaffold
fun HomeScreen(navController: NavHostController = rememberNavController()) {
Scaffold(bottomBar = {
BottomNavigation(navController = navController)
}) {
HomeNavGraph(navHostController = navController)
}
at last in the rootgraph use this homescreen class with your rootgraph route
fun RootNavigationGraphBuilder(navHostController: NavHostController) {
NavHost(navController = navHostController, startDestination = Graph.Authentication, route = Graph.Root) {
authNavGraph(navHostController)
composable(route = Graph.Home){
HomeScreen()
}
}
here are sample routes which i use in my code
object Graph {
const val Root = "root_graph"
const val Authentication = "auth_graph"
const val Home = "home_graph"
}

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.

Categories

Resources