I am developing an application in which I have added a side navigation menu. I have also added the menu icon in the TopBar. When this button is pressed, the side menu is displayed with its options (Data and About).
I don't know how to achieve the following:
When I am in the Data window I want the TopBar icon to be the menu or hamburger icon
When in the About window I want the TopBar icon to be the back arrow
When you are in the About window and click on the back arrow of step 2, I want you to return to the Data window
With the attached code, I can navigate between the Data and About window, but I have no way to go back, except with Android's own button in the bottom bar. My intention is to be able to navigate with the TopBar button
MainActivity
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: MyViewModel by viewModels()
#ExperimentalMaterialApi
#ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = true
setContent {
MyTheme {
Surface(color = MaterialTheme.colors.background) {
MainScreen(viewModel = viewModel)
}
}
}
}
}
#ExperimentalAnimationApi
#ExperimentalMaterialApi
#Composable
fun MainScreen(
viewModel: MyViewModel
) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(
drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
)
val scope = rememberCoroutineScope()
val navigationItems = listOf(
Screen.DataScreen,
Screen.SettingsScreen
)
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopBar(navController = navController, scope = scope, scaffoldState = scaffoldState) },
drawerContent = { Drawer(scope = scope, scaffoldState = scaffoldState, navController = navController, items = navigationItems) },
drawerGesturesEnabled = true
) {
NavigationHost(navController, viewModel, params)
}
}
TopBar:
#Composable
fun TopBar(
navController: NavHostController,
scope: CoroutineScope,
scaffoldState: ScaffoldState
) {
TopAppBar(
title = {
Text(
stringResource(R.string.app_name),
fontWeight = FontWeight.Bold
)
},
navigationIcon =
{
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu Icon")
}
}
)
}
Drawer:
#Composable
fun Drawer(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
navController: NavHostController,
items: List<Screen>
) {
val currentRoute = currentRoute(navController)
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
Column {
Text(
text = stringResource(id = R.string.app_name),
color = Color.White
)
Text(
text = stringResource(id = R.string.app_description),
color = Color.Black
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
) {
Column {
items.forEach { item ->
DrawerItem(item = item, selected = currentRoute == item.route) {
navController.navigate(item.route) {
launchSingleTop = true
}
scope.launch {
scaffoldState.drawerState.close()
}
}
}
}
}
}
}
#Composable
fun DrawerItem(
item: Screen,
selected: Boolean,
onItemClick: (Screen) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onItemClick(item) }
) {
Icon(
modifier = Modifier.size(32.dp),
imageVector = item.icon,
contentDescription = item.title
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = item.title
)
}
}
NavigationHost:
#Composable
fun NavigationHost(
navController: NavHostController,
viewModel: MyViewModel
) {
NavHost(navController = navController, startDestination = Screen.DataScreen.route) {
composable(Screen.DataScreen.route) {
DataScreen(
navController = navController,
viewModel = viewModel
)
}
composable(Screen.SettingsScreen.route) {
SettingsScreen(
navController = navController
)
}
}
}
Screen:
sealed class Screen(
val route: String,
val title: String,
val icon: ImageVector
) {
object DataScreen :
Screen("data_screen", "Data", Icons.Filled.LocationOn)
object SettingsScreen :
Screen("settings_screen", "About", Icons.Filled.Info)
}
Related
I have a problem with navigation in my Compose app. So starting from the beginning I want to have two navigation graphs: one for authentication related things and second for main functionalities, which should be accessible only after login. So typical case.
I want to use Splash Screen API from Android 12, so in my main activity I could do something like this:
class AppMainActivity : ComponentActivity() {
private val viewModel by viewModels<SplashViewModel>()
private var userAuthState: UserAuthState = UserAuthState.UNKNOWN
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onEvent(SplashEvent.CheckAuthentication)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect {
userAuthState = it
}
}
}
setContent {
CarsLocalizerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
val splash = installSplashScreen()
splash.setKeepOnScreenCondition {
userAuthState != UserAuthState.UNKNOWN
}
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
}
}
}
}
}
}
Here I am collecting StateFlow from view model, which describes if user is authenticated already or no. If authenticated successfully then go to HomeScreen, which has HomeNavigation inside. If not authenticated, go to Authentication nav graph. Problem is that this approach would not work, since activity is created only once, so if I user will login, this when
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
Won’t be called again.
I was trying to find some solution for my problem but, can’t find anything helpful. Maybe somebody had such issue before, or saw something useful? Will be very glad for any help.
Rest of my code:
SplashViewModel
#HiltViewModel
class SplashViewModel #Inject constructor(
private val authenticateUseCase: AuthenticateUseCase
): ViewModel() {
private val _isAuthenticated = MutableStateFlow<UserAuthState>(value = UserAuthState.UNKNOWN)
val isAuthenticated: StateFlow<UserAuthState> = _isAuthenticated.asStateFlow()
fun onEvent(event: SplashEvent) {
when (event) {
is SplashEvent.CheckAuthentication -> {
viewModelScope.launch {
val result = authenticateUseCase()
when (result) {
true -> {
_isAuthenticated.emit(UserAuthState.AUTHENTICATED)
}
false -> {
_isAuthenticated.emit(UserAuthState.UNAUTHENTICATED)
}
}
}
}
}
}
}
AuthNavigation
#Composable
fun AuthNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState
) {
NavHost(
navController = navController,
startDestination = Screen.Login.route,
modifier = Modifier.fillMaxSize()
) {
composable(Screen.Login.route) {
LoginScreen(
onNavigate = { navController.navigate(it) } ,
onLogin = {
navController.popBackStack()
},
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Register.route) {
RegisterScreen(
onPopBackstack = { navController.popBackStack() },
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Onboarding.route) {
OnboardingScreen(
onCompleted = { navController.popBackStack() },
viewModel = hiltViewModel()
)
}
}
}
HomeScreen
#Composable
fun HomeScreen(
viewModel: HomeViewModel,
onLogout: () -> Unit
) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
var appBarTitle by remember {
mutableStateOf("")
}
LaunchedEffect(key1 = true) {
viewModel.userName.collectLatest {
appBarTitle = "Hello $it"
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
if (appBarTitle.isEmpty()) {
AppBarWithName("Hello")
} else {
AppBarWithName(appBarTitle)
}
},
bottomBar = { BottomNavigationBar(navController) },
content = { padding ->
Box(modifier = Modifier.padding(padding)) {
HomeNavigation(
navController = navController,
scaffoldState = scaffoldState,
onLogout = { onLogout() }
)
}
}
)
}
HomeNavigation
#Composable
fun HomeNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState,
onLogout: () -> Unit
) {
NavHost(
navController = navController,
startDestination = Screen.Map.route
) {
composable(Screen.Map.route) {
MapScreen(viewModel = hiltViewModel())
}
composable(Screen.ManageCars.route) {
ManageCarsScreen(
viewModel = hiltViewModel(),
scaffoldState = scaffoldState,
onAddCar = {
navController.navigate(Screen.AddCar.route)
}
)
}
composable(Screen.AddCar.route) {
AddCarScreen(
viewModel = hiltViewModel(),
onPopBackstack = {
navController.popBackStack()
}
)
}
composable(Screen.Logout.route) {
LogoutDialogScreen(
viewModel = hiltViewModel(),
onLogout = {
navController.popBackStack()
onLogout()
},
onCancel = {
navController.popBackStack()
}
)
}
}
}
You need to store the UserAuthState as MutableState, this way, when the value is updated, setContent will automatically re-compose:
private var userAuthState = mutableStateOf(UserAuthState.UNKNOWN)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect { userAuthState.value = it }
}
}
setContent {
when (userAuthState.value) {
etc....
}
}
From the docs:
mutableStateOf creates an observable MutableState, which is an
observable type integrated with the compose runtime.
Any changes to value schedules recomposition of any composable
functions that read value.
I just learned Jetpack Compose and building a simple login screen with retrofit to connect with the API.
I'm able to navigate from login screen to home screen. But I'm wondering if I'm doing it right.
Here is my login screen composable
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun InsertNumberScreen(
modifier: Modifier = Modifier,
navHostController: NavHostController,
viewModel: LoginViewModel = viewModel(factory = LoginViewModel.provideFactory(
navHostController = navHostController,
owner = LocalSavedStateRegistryOwner.current
)),
) {
var phoneNumber by remember {
mutableStateOf("")
}
var isActive by remember {
mutableStateOf(false)
}
val modalBottomSheetState =
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val coroutine = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetLoginContent(phoneNumber){
//Here I call login function inside viewModel
viewModel.login(phoneNumber)
}
},
sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
Column {
TopAppBarCustom(text = "")
LoginText(modifier = modifier.padding(16.dp))
Row(modifier = modifier.padding(16.dp)) {
Prefix()
PhoneNumber(
shape = RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp),
value = phoneNumber,
onValueChange = {
isActive = it.length >= 10
phoneNumber = it
})
}
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
)
BottomContainer(isEnabled = isActive) {
coroutine.launch {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.animateTo(ModalBottomSheetValue.Hidden)
} else {
modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
}
}
}
}
}
}
Here is my ViewModel
class LoginViewModel(val navHostController: NavHostController) : ViewModel() {
var result by mutableStateOf(Data(0, "", Message("", "")))
fun login(phone: String) {
val call: Call<Data> = Network.NetworkInterface.login(phone)
call.enqueue(
object : Callback<Data> {
override fun onResponse(call: Call<Data>, response: Response<Data>) {
if (response.code() == 400) {
val error =
Gson().fromJson(response.errorBody()!!.charStream(), Data::class.java)
result = error
navHostController.navigate("login")
} else {
result = response.body()!!
navHostController.navigate("home")
}
}
override fun onFailure(call: Call<Data>, t: Throwable) {
Log.d("Data Login", t.message.toString())
}
}
)
}
companion object {
fun provideFactory(
navHostController: NavHostController,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null,
): AbstractSavedStateViewModelFactory =
object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return LoginViewModel(navHostController) as T
}
}
}
}
In my viewModel class, it has a constructor NavHostController. And then, in the login method, I call navHostController.navigate() to navigate to home screen if the login is success.
The question is, is it okay to call navHostController.navigate() directly inside the viewModel? Because I follow codelabs from Google and the navigation is handled in the sort of NavHostBootstrap composable (Something like this)
#Composable
fun RallyNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
){
NavHost(navController = navController, startDestination = Overview.route, modifier = modifier){
composable(Overview.route){
OverviewScreen(
onClickSeeAllAccounts = {
navController.navigateSingleTopTo(Accounts.route)
},
onClickSeeAllBills = {
navController.navigateSingleTopTo(Bills.route)
},
onAccountClick = {
Log.d("Account Clicked", it)
navController.navigateToSingleAccount(it)
}
)
}
}
I want to navigate to another screen after clicking on the IconButton in TaskItem.kt but it doesn't work. Logcat doesn't show anything, there aren't any errors in "Run". After clicking the edit IconButton it should navigate to another screen.
Screen.kt:
package com.example.planner
sealed class Screen(val route: String) {
object TasksList: Screen(route = "tasks_list")
object AddScreen: Screen(route = "add_screen")
object UpdateScreen: Screen(route = "update_screen")
}
NavGraph.kt:
package com.example.planner
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
#Composable
fun Navigation(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.TasksList.route)
{
composable(
route = Screen.TasksList.route
) {
TasksList(navController = navController)
}
composable(
route = Screen.AddScreen.route
) {
AddScreen(navController = navController)
}
composable(
route = Screen.UpdateScreen.route
) {
UpdateScreen(navController = navController)
}
}
}
Part of TaskItem.kt:
IconButton(onClick = { navController.navigate(route = Screen.UpdateScreen.route) }, modifier = Modifier.size(36.dp)) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "Edit",
modifier = Modifier.padding(horizontal = 2.dp),
tint = MaterialTheme.colorScheme.onBackground
)
}
MainActivity.kt:
class MainActivity : ComponentActivity() {
private lateinit var navController: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PlannerTheme {
// A surface container using the "background" color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
navController = rememberNavController()
Navigation(navController = navController)
MyApp(Modifier.fillMaxSize(), navController)
}
}
}
}
}
#Composable
fun MyApp(modifier: Modifier = Modifier, navController: NavController) {
Surface(modifier, color = MaterialTheme.colorScheme.background) {
TasksList(navController = navController)
}
}
#Preview(name = "LightMode", showBackground = true)
#Preview(name = "DarkMode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
#Preview(name = "FullPreview", showBackground = true, showSystemUi = true)
#Composable
fun DefaultPreview() {
PlannerTheme {
Surface(modifier = Modifier, color = MaterialTheme.colorScheme.background) {
TasksList(navController = rememberNavController())
}
}
}
MyApp(Modifier.fillMaxSize(), navController),Is this sentence redundant? Just remove it.
I've read the article about handling deeplinks with Compose,
but I didn't find anything for my use case - for navigation within LazyColumn.
I have the following main Composable which I want to make the root of the graph (I want it to be host for NavHost):
#Composable
private fun MainScreenContainer(
mainState: state,
...
) {
val navController = rememberNavController()
MainTheme {
Box(
modifier = Modifier.fillMaxSize()
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
mainScreenContent(
state = mainState,
...
)
}
StickyToolbar(
...
)
}
}
}
and here is the remaining code:
private fun LazyListScope.mainScreenContent(
mainState: MainState,
...
) {
header(...)
overview(...)
myCard(...)
footer()
}
private fun LazyListScope.myCard(
...
) {
item {
ContentContainer(
modifier = Modifier.padding(vertical = MainTheme.spacing.medium)
) {
MyCardStateless(
onCardClick = myCardViewModel::onCardClick,
...
)
}
}
}
#Composable
fun MyCardStateless(
onCardClick: () -> Unit,
...
) {
Column(modifier = modifier) {
Card(
modifier = Modifier.fillMaxWidth()
.clickable(onClick = onCardClick)
) { ... }
}
}
And within MyCardViewModel I was using navigation to another fragment via Navigation Component:
fun onCardClick() {
navigate(
MainFragmentDirections.actionMainFragmentToDescriptionFragment()
)
}
#AndroidEntryPoint
class DescriptionFragment : ...() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
setContent {
DescriptionStateful(...)
}
}
...
}
But now I want to add 2 deeplinks ("myapp://mycard" - to MyCardStateless and "myapp://description" - to DescriptionFragment or to DescriptionStateful) and handle navigation using navigation-compose. Is it possible at all?
My app has a main screen with a Scaffold and a BottomNavigation bar:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MyApplicationTheme {
Scaffold(
bottomBar = {
BottomBar(navController = navController)
}
) {
NavigationGraph(navController = navController)
}
}
}
}
...
#Composable
fun NavigationGraph(navController: NavHostController){
NavHost(navController = navController, startDestination = BottomMenuOption.Home.route) {
composable(route = BottomMenuOption.Home.route) {
HomeScreen(navController = navController)
}
composable(route = BottomMenuOption.Settings.settings) {
SettingsScreen()
}
composable(route = BottomMenuOption.Profile.route) {
ProfileScreen()
}
composable(route = "feature") {
FeatureScreen()
}
}
}
FeatureScreen has it's own Scaffold with a topBar instead of a bottomBar and when I navigate to it from HomeScreen, I want to replace the previous one from the main screen and just see a topBar but instead, I'm seeing the two bars in the screen.
#Composable
fun FeatureScreen() {
Scaffold (
topBar = {
TopBar(" Feature Screen")
}
) {
}
}
Is it possible to accomplish this? I'm thinking it could be done by just using a new Activity but ideally, I would like to keep the Single Activity approach.
I would suggest creating a new function like this:
#Composable
fun MainScaffold(
topBar: #Composable (() -> Unit) = {},
bottomBar: #Composable (() -> Unit) = {},
content: #Composable (PaddingValues) -> Unit){
Scaffold(
bottomBar = bottomBar,
topBar = topBar,
content = content
)
}
then, use this main scaffold in your screens:
#Composable
fun HomeScreen(navController: NavHostController) {
MainScaffold(
bottomBar = { BottomBar(navController = navController) },
content = {
// content
})
}
and in your feature screen:
#Composable
fun FeatureScreen() {
MainScaffold (
topBar = {
TopBar(" Feature Screen")
}
) {
//content
}
}
and in setContent
setContent {
val navController = rememberNavController()
VoiceAssistantJetpackComposeTheme {
NavigationGraph(navController = navController)
}}