I am trying to build below navigation for my order management app:
manage_orders/manage_orders/{locationId}
manage_orders/manage_order_details/{orderId}
And here is my navigation code for that:
internal sealed class Screen(val route: String) {
object ManageOrders : Screen(manage_orders)
}
private sealed class LeafScreen(val route: String) {
fun createRoute(root: Screen): String {
return "${root.route}/$route"
}
object ManageOrders : LeafScreen("manage_orders/{locationId}") {
fun createRoute(root: Screen, locationId: String): String {
return "${root.route}/manage_orders/$locationId"
}
}
object ManageOrderDetails : LeafScreen("manage_order_details/{orderId}") {
fun createRoute(root: Screen, orderId: String): String {
return "${root.route}/manage_order_details/$orderId"
}
}
}
#ExperimentalCoroutinesApi
#Composable
internal fun AppNavigation(
navController: NavHostController,
locationId: String,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = Screen.ManageOrders.route,
modifier = modifier,
) {
addManageOrdersTopLevel(navController, locationId)
}
}
#ExperimentalCoroutinesApi
private fun NavGraphBuilder.addManageOrdersTopLevel(
navController: NavHostController,
locationId: String
) {
navigation(
route = Screen.ManageOrders.route,
startDestination = LeafScreen.ManageOrders.createRoute(Screen.ManageOrders, locationId)
) {
addManageOrders(navController = navController, root = Screen.ManageOrders)
addManageOrderDetails(navController = navController, root = Screen.ManageOrders)
}
}
#ExperimentalCoroutinesApi
private fun NavGraphBuilder.addManageOrders(
navController: NavHostController,
root: Screen
) {
composable(
route = LeafScreen.ManageOrders.createRoute(root),
arguments = listOf(
navArgument(LOCATION_ID) { type = NavType.StringType }
)
) { backStackEntry ->
backStackEntry.arguments?.let {
ManageOrders(locationId = it.getString(LOCATION_ID)!!) { orderId ->
navController.navigate(LeafScreen.ManageOrderDetails.createRoute(root, orderId))
}
}
}
}
#ExperimentalCoroutinesApi
private fun NavGraphBuilder.addManageOrderDetails(
navController: NavHostController,
root: Screen
) {
composable(
route = LeafScreen.ManageOrderDetails.createRoute(root),
arguments = listOf(
navArgument(ORDER_ID) { type = NavType.StringType }
)
) { backStackEntry ->
backStackEntry.arguments?.let {
ManageOrderDetails(
navController = navController,
orderId = it.getString(ORDER_ID)
)
}
}
}
And here is the code to start the navigation:
class ManageOrderActivity : AppCompatActivity() {
#ExperimentalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
AppNavigation(
navController = navController,
locationId = intent.extras?.getString(KEY_LOCATION_ID) ?: ""
)
}
}
}
However, I am getting below error:
FATAL EXCEPTION: main
Process: io.chanse.locals.cerve.qa, PID: 18285
java.lang.IllegalArgumentException: navigation destination -1881727488 is not a direct child of this NavGraph
at androidx.navigation.NavGraphNavigator.navigate(NavGraphNavigator.kt:72)
at androidx.navigation.NavGraphNavigator.navigate(NavGraphNavigator.kt:49)
at androidx.navigation.NavController.navigateInternal(NavController.kt:189)
at androidx.navigation.NavController.navigate(NavController.kt:1491)
at androidx.navigation.NavController.onGraphCreated(NavController.kt:913)
at androidx.navigation.NavController.setGraph(NavController.kt:852)
at androidx.navigation.NavController.setGraph(NavController.kt:90)
at androidx.navigation.compose.NavHostKt$NavHost$4.invoke(NavHost.kt:113)
at androidx.navigation.compose.NavHostKt$NavHost$4.invoke(NavHost.kt:112)
at androidx.compose.runtime.DisposableEffectImpl.onRemembered(Effects.kt:81)
at androidx.compose.runtime.CompositionImpl$RememberEventDispatcher.dispatchRememberObservers(Composition.kt:781)
at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:639)
at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:733)
at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:432)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:144)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:135)
at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:727)
at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:135)
at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:187)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:196)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:142)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:135)
at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:814)
at android.view.View.dispatchAttachedToWindow(View.java:22010)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4291)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4298)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4298)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4298)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4298)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4298)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4298)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3135)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2618)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9971)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1010)
at android.view.Choreographer.doCallbacks(Choreographer.java:809)
at android.view.Choreographer.doFrame(Choreographer.java:744)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:995)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8512)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1139)
What's the problem here?
Update 1 (as per Ian's solution)
#Composable
internal fun AppNavigation(
navController: NavHostController,
locationId: String,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = LeafScreen.ManageOrders.route,
modifier = modifier,
) {
addManageOrdersTopLevel(navController, locationId)
}
}
#ExperimentalCoroutinesApi
private fun NavGraphBuilder.addManageOrdersTopLevel(
navController: NavHostController,
locationId: String
) {
navigation(
route = Screen.ManageOrders.route,
startDestination = LeafScreen.ManageOrders.createRoute(Screen.ManageOrders)
) {
addManageOrders(
navController = navController,
root = Screen.ManageOrders,
locationId = locationId
)
}
}
private fun NavGraphBuilder.addManageOrders(
navController: NavHostController,
root: Screen,
locationId: String
) {
composable(
route = LeafScreen.ManageOrders.createRoute(root),
arguments = listOf(
navArgument(LOCATION_ID) {
type = NavType.StringType
defaultValue = locationId
}
)
) { backStackEntry ->
backStackEntry.arguments?.let {
ManageOrders(locationId = it.getString(LOCATION_ID)!!) { orderId ->
navController.navigate(LeafScreen.ManageOrderDetails.createRoute(root, orderId))
}
}
}
}
But still facing the same issue. Look's like I failed to understand what Ian sggested. What have I missed?
This line:
startDestination = LeafScreen.ManageOrders.createRoute(Screen.ManageOrders, locationId)
Does not match any of the route parameters on your destination. For example, your Screen.ManageOrders's route is:
route = LeafScreen.ManageOrders.createRoute(root)
The startDestination needs to match a route exactly. That means you need to be using
startDestination = LeafScreen.ManageOrders.createRoute(root)
If you want to set a locationId to be used for your start destination, you should set a defaultValue on your argument:
#ExperimentalCoroutinesApi
private fun NavGraphBuilder.addManageOrdersTopLevel(
navController: NavHostController,
locationId: String
) {
navigation(
route = Screen.ManageOrders.route,
startDestination = LeafScreen.ManageOrders.createRoute(root)
) {
addManageOrders(
navController = navController,
root = Screen.ManageOrders,
locationId = locationId
)
addManageOrderDetails(navController = navController, root = Screen.ManageOrders)
}
}
#ExperimentalCoroutinesApi
private fun NavGraphBuilder.addManageOrders(
navController: NavHostController,
root: Screen,
locationId: String
) {
composable(
route = LeafScreen.ManageOrders.createRoute(root),
arguments = listOf(
navArgument(LOCATION_ID) {
type = NavType.StringType
defaultValue = locationId
}
)
) { backStackEntry ->
backStackEntry.arguments?.let {
ManageOrders(locationId = it.getString(LOCATION_ID)!!) { orderId ->
navController.navigate(LeafScreen.ManageOrderDetails.createRoute(root, orderId))
}
}
}
}
In my case the problem was I didn't include the route inside my NavGraphBuilder->
Before->
KoinNav(navController) {
AnimatedNavHost(
navController,
startDestination =
MainScreenItems.ContactListScreenItem.route
) {
composable(route = Item.A.route) {
contentA()
}
composable(route = Item.A.route) {
contentB()
}
}
After->
KoinNav(navController) {
AnimatedNavHost(
navController,
startDestination =
MainScreenItems.ContactListScreenItem.route
) {
composable(route = Item.A.route) {
contentA()
}
composable(route = Item.B.route) {
contentB()
}
}
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)
}
)
}
}
Recently in my app I've been using simple navigation component arguments passing. Since I've added Hilt ViewModel, i came across something called saveStateHandle and apparently I can pass arguments between screens easly with this. How can i do that? I implemented the code in my HiltViewModel
#HiltViewModel
class ExerciseViewModel #Inject constructor(
private val repository: ExerciseRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
val data: MutableState<DataOrException<List<Exercise>, Boolean, Exception>> =
mutableStateOf(
DataOrException(null, true, Exception(""))
)
val muscleGroup: String? = savedStateHandle[Constants.MUSCLE_GROUP_KEY]
private val _exerciseListFromDb = MutableStateFlow<List<Exercise>>(emptyList())
val exerciseListFromDb = _exerciseListFromDb.asStateFlow()
init {
getExercises()
viewModelScope.launch(Dispatchers.IO) {
repository.getAllExercisesFromDb().collect() {
_exerciseListFromDb.value = it
}
}
}
private fun getExercises() {
viewModelScope.launch {
data.value.loading = true
data.value = repository.getExercises()
if (data.value.data.toString().isNotEmpty())
data.value.loading = false
}
}
fun insertExerciseToDb(exercise: Exercise) = viewModelScope.launch {
repository.insertExerciseToDb(exercise)
}
fun deleteExerciseFromDb(exercise: Exercise) = viewModelScope.launch {
repository.deleteExerciseFromDb(exercise)
}
}
I want to pass muscleGroup parameter between screens HomeScreen -> SampleExercisesScreen. How do I send parameter from HomeScreen to HiltViewModel ExerciseViewModel and then use it in SampleExercisesScreen and other screens?
#Composable
fun HomeScreen(navController: NavController) {
Surface(modifier = Modifier.fillMaxSize(),
color = AppColors.mBackground) {
Column {
Header()
Row(modifier = Modifier
.fillMaxWidth()
.padding(top = 50.dp)){
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.body, muscleGroup = "Chest", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.male, muscleGroup = "Back", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.shoulder, muscleGroup = "Shoulders", navController)
}
Row(modifier = Modifier.fillMaxWidth()){
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.muscle, muscleGroup = "Biceps", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.triceps, muscleGroup = "Triceps", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.leg, muscleGroup = "Legs", navController)
}
}
}
},
#Composable
fun SampleExerciseScreen(navController: NavController, muscleGroup: String, exerciseList: List<Exercise>?) {
val mExerciseList = exerciseList!!.filter { it.muscle == muscleGroup }
Log.d("TEST", "$mExerciseList, $muscleGroup")
Surface(modifier = Modifier.fillMaxSize(),
color = AppColors.mBackground) {
Column {
MyTopBar(navController = navController)
LazyColumn(Modifier.weight(1f)){
items(mExerciseList) {
ExerciseRow(exercise = it)
}
}
GoToButton(navController = navController, text = "YOUR EXERCISES", route = Screen.UserExercises.passMuscleGroup(muscleGroup))
}
}
}
NavGraph
#Composable
fun SetupNavGraph(navController: NavHostController, viewModel: ExerciseViewModel) {
val exerciseList = viewModel.data.value.data?.toList()
val exerciseListFromDb = viewModel.exerciseListFromDb.collectAsState().value
val muscleGroup = viewModel.muscleGroup
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(
route = Screen.Home.route
) {
HomeScreen(navController = navController)
}
composable(
route = Screen.SampleExercise.route,
) {
SampleExerciseScreen(
navController = navController,
muscleGroup = muscleGroup.toString(),
exerciseList = exerciseList
)
}
composable(
route = Screen.UserExercises.route,
arguments = listOf(navArgument(MUSCLE_GROUP_KEY) {
type = NavType.StringType
})
) {
UserExercisesScreen(
navController = navController,
muscleGroup = it.arguments?.getString(MUSCLE_GROUP_KEY).toString(),
viewModel = viewModel,
exerciseListFromDb = exerciseListFromDb
)
}
composable(
route = Screen.Add.route,
arguments = listOf(navArgument(MUSCLE_GROUP_KEY) {
type = NavType.StringType
})
) {
AddScreen(
navController = navController, muscleGroup = it.arguments?.getString(MUSCLE_GROUP_KEY).toString(),
viewModel = viewModel
)
}
}
}
I have a blog post here that explains it: https://www.francescvilarino.com/passing-arguments-to-screens-in-jetpack-compose
But basically you have to retrieve the arguments in the viewmodel from the SavedStateHandle, using the key that you used in your route for each argument.
navController.navigate(buildTwoRoute(argument))
then
#HiltViewModel
class TwoViewModel #Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
init {
val argument = savedStateHandle.get<String>(DestinationOneArg).orEmpty()
}
}
When attempting to declare code to animate between two different composables, a 'deprecated' error is returned. How can this be fixed and cleared?
AnimatedVisibility(Boolean, Modifier = ..., EnterTransition, ExitTransition, Boolean, () -> Unit): Unit' is deprecated. AnimatedVisibility no longer accepts initiallyVisible as a parameter, please use AnimatedVisibility(MutableTransitionState, Modifier, ...) API instead
class MainActivity : ComponentActivity() {
private lateinit var navController: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
navController = rememberNavController()
MyNavGraph(navController = navController)
}
}
}
#Composable
fun EnterAnimation(content: #Composable () -> Unit) {
AnimatedVisibility(
visible = true,
enter = slideInVertically(
initialOffsetY = { -30 }
) + expandVertically(
expandFrom = Alignment.Top
) + fadeIn(initialAlpha = 0.3f),
exit = slideOutVertically() + shrinkVertically() + fadeOut(),
content = content,
initiallyVisible = false
)
}
#Composable
fun MyNavGraph(
navController: NavHostController
) {
NavHost(
navController = navController,
startDestination = Screen.Shoes.route
) {
composable(route = Screen.Shoes.route){
ShoesScreen()
}
composable(route = Screen.Brands.route){
BrandsScreen()
}
}
}
#Composable
fun ShoesScreen() {...}
#Composable
fun BrandsScreen() {...}
}
That particular function is deprecated. You can use the new api which use MutableTransitionState instead of plain boolean.
import androidx.compose.animation.core.MutableTransitionState
#Composable
fun EnterAnimation(content: #Composable () -> Unit) {
val initialVisible = true
AnimatedVisibility(
visibleState = remember { MutableTransitionState(initialVisible) },
enter = slideInVertically(
) + expandVertically(
) + fadeIn(),
) {
content()
}
}
You can also do like this by adding AnimatedVisibilityScope to the composable type,
fun EnterAnimation(content: #Composable AnimatedVisibilityScope.() -> Unit) {
val initialVisible = true
AnimatedVisibility(
visibleState = remember { MutableTransitionState(initialVisible) },
enter = slideInVertically(
) + expandVertically(
) + fadeIn(),
content = content)
}
I have a very simple application ui made with Compose.
3 screens for now: login, items list and item details.
The problem is when i click on an item in the items list, i navigate to the item details and i pass the item ID as nav arg.
The problem is, I can't retrieve it and the arguments bundle is empty.
Here is my code:
MainActivity:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AdrestoApplication()
}
}
}
Main Composable:
#Composable
fun AdrestoApplication() {
val authViewModel = hiltViewModel<AuthViewModel>()
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val isLoggedIn = authViewModel.isLoggedIn()
val initialRoute =
if (isLoggedIn) Destinations.LISTINGS_LIST else Destinations.LOGIN_ROUTE
val currentRoute = navBackStackEntry?.destination?.route ?: initialRoute
AdrestoTheme {
NavGraph(
navController = navController,
startDestination = currentRoute,
)
}
}
NavGraph:
object Destinations {
const val LOGIN_ROUTE = "login"
const val HOME_ROUTE = "home"
const val ORDER_DETAILS = "order_details"
const val ORDERS_LIST = "orders_list"
const val LISTING_DETAILS = "listing_details/{listingId}"
const val LISTINGS_LIST = "listings_list"
}
#Composable
fun NavGraph(
navController: NavHostController = rememberNavController(),
startDestination: String = Destinations.LOGIN_ROUTE,
) {
NavHost(navController = navController, startDestination = startDestination) {
composable(Destinations.LOGIN_ROUTE) {
Login(
navController = navController,
)
}
composable(Destinations.LISTINGS_LIST) {
ListingsScreen(
navController = navController,
)
}
composable(
Destinations.LISTING_DETAILS,
arguments = listOf(navArgument("listingId") { type = NavType.LongType })
) {
Log.d("Navhost","NavGraph: the nav arg found is: ${it.arguments!!.getLong("listingId")}") // logs 0
ListingScreen(
navController = navController,
listingId = it.arguments!!.getLong("listingId"),
)
}
}
}
The part of the UI that triggers the navigation with argument:
LazyColumn(
modifier = Modifier
.padding(all = 8.dp)
.background(MaterialTheme.colors.background)
) {
items(listings.value.data ?: listOf()) { listing ->
Listing(
listing = listing,
onCardClick = {
Log.d("some tag", "ListingsScreen: navigating to ${listing.id}") // logs as expected
val route = "listing_details/${listing.id}"
Log.d("some tag", "ListingsScreen: the route is $route") //logs listing_details/418 (for example)
navController.navigate(route)
},
)
}
}
I can't understand what's going wrong here, isn't the composables in the same navgraph?
I have another issue which is when i press the back button, the navigation don't go up but it closes the app (like if there is not backstack...).
The problem was in the start destination argument of the NavGraph, I was reinitializing (unkowingly) the navController backstack each time I navigated to a new screen.
#Composable
fun AdrestoApplication() {
val authViewModel = hiltViewModel<AuthViewModel>()
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val isLoggedIn = authViewModel.isLoggedIn()
val initialRoute =
if (isLoggedIn) Destinations.LISTINGS_LIST else Destinations.LOGIN_ROUTE
val currentRoute = navBackStackEntry?.destination?.route ?: initialRoute
AdrestoTheme {
NavGraph(
navController = navController,
startDestination = currentRoute, // this was issue, changed to a static initial route.
)
}
}