How to use Viewmodel properly in Jetpack Compose Navigation - android

I'm currently building an app with Jetpack Compose and Some other Jetpack Libraries,
and I use Room for storing data like this
#Dao
interface ClassDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertClassList(classes: List<ClassData>)
#Query("SELECT * FROM ClassData WHERE id=:id")
fun getClassList(id: String): Flow<List<ClassData>>
}
#Database(
entities = [ClassData::class],
version = 1,
exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun classDao(): ClassDao
}
and I use Repository for remote and local model integration like this
class ResourceRepository
#Inject
constructor(
private val userPreference: UserPreference,
private val classDao: ClassDao
) {
fun getClassList() = classDao.getClassList(userPreference.getCachedUserId()).flowOn(Dispatchers.IO)
}
and I use Hilt for dependency injection like this
#Module
#InstallIn(SingletonComponent::class)
object PersistenceModule {
#Provides
#Singleton
fun provideAppDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(
application, AppDatabase::class.java, application.getString(R.string.database))
.fallbackToDestructiveMigration()
.build()
}
#Provides
#Singleton
fun provideClassDao(appDatabase: AppDatabase): ClassDao {
return appDatabase.classDao()
}
}
#Module
#InstallIn(ViewModelComponent::class)
object RepositoryModule {
#Provides
#ViewModelScoped
fun provideResourceRepository(
apiService: ApiService,
userPreference: UserPreference,
classDao: ClassDao
): ResourceRepository {
return ResourceRepository(
apiService,
userPreference,
classDao)
}
}
then I create Viewmodel for communicate data with Composable
#HiltViewModel
class MainViewModel #Inject constructor(private val resourceRepository: ResourceRepository) : ViewModel() {
private val _toast: MutableLiveData<String> = MutableLiveData("")
val toast: LiveData<String>
get() = _toast
val classList = resourceRepository.getClassList()
}
Then I create my MainActivity layout with Jetpack Compose and JetPack Compose Navigation, using BottomNavigation with NavHost to build a traditional BottomNavigation Activity
#Composable
fun Mobile4Main() {
val viewModel = hiltViewModel<MainViewModel>()
val context = LocalContext.current
LocalLifecycleOwner.current.let { owner ->
viewModel.toast.observe(owner) {
if (it.isNotBlank()) {
ToastUtil.show(context, it)
}
}
}
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Open))
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopAppBar(title = { Text("Home") }) },
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(onClick = { viewModel.getResources() }) {
Icon(Icons.Filled.Refresh, "", tint = MaterialTheme.colors.background)
}
},
bottomBar = { MainBottomNavigation(navController) })
{ innerPadding ->
MainNavHost(navController, viewModel, innerPadding)
}
}
#Composable
fun MainBottomNavigation(
navController: NavHostController
) {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
onClick = {
if (currentRoute != screen.route) {
navController.navigate(screen.route) {
navController.graph.startDestinationRoute?.let {
popUpTo(it) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
})
}
}
}
#Composable
fun MainNavHost(
navController: NavHostController,
mainViewModel: MainViewModel,
innerPadding: PaddingValues
) {
NavHost(
navController,
startDestination = Screen.ClassList.route,
Modifier.padding(innerPadding)
) {
composable(Screen.ClassList.route) {
ClassPage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.ExamList.route) {
ExamPage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.ScoreList.route) {
ScorePage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.Statistics.route) {
StatisticsPage(mainViewModel, Modifier.fillMaxHeight())
}
}
}
sealed class Screen(val route: String, #StringRes val resourceId: Int, val icon: ImageVector) {
object ClassList :
Screen("classList", R.string.class_bottom_navigation_item, Icons.Filled.Class)
object ExamList :
Screen("examList", R.string.exam_bottom_navigation_item, Icons.Filled.Dashboard)
object ScoreList :
Screen("scoreList", R.string.score_bottom_navigation_item, Icons.Filled.Score)
object Statistics :
Screen("statistics", R.string.statistics_bottom_navigation_item, Icons.Filled.Star)
}
val items = listOf(Screen.ClassList, Screen.ExamList, Screen.ScoreList, Screen.Statistics)
One of the page are like this, using Flow.collectAsState() to convert data Flow from Room to Composable State
#Composable
fun ClassPage(
viewModel: MainViewModel,
modifier: Modifier = Modifier
) {
val classesData by viewModel.classList.collectAsState(listOf())
ClassList(classesData, modifier)
}
#Composable
fun ClassList(classesData: List<ClassData>, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)
) {
LazyColumn(state = listState, contentPadding = PaddingValues(4.dp)) {
items(
items = classesData,
itemContent = { classData -> ClassItem(classData = classData, selectClass = {}) })
}
}
}
And it did build a workable MainActivity with BottomNavigation, but when I switch between BottomNavigation buttons quickly, my app crashed and I get Error Log like below:
Process: ***, PID: 26668
java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry reaches the CREATED state).
at androidx.navigation.NavBackStackEntry.getViewModelStore(NavBackStackEntry.kt:174)
at androidx.lifecycle.ViewModelProvider.<init>(ViewModelProvider.java:99)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:82)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:72)
at androidx.navigation.compose.NavBackStackEntryProviderKt.SaveableStateProvider(NavBackStackEntryProvider.kt:86)
at androidx.navigation.compose.NavBackStackEntryProviderKt.access$SaveableStateProvider(NavBackStackEntryProvider.kt:1)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.invoke(NavBackStackEntryProvider.kt:51)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.invoke(NavBackStackEntryProvider.kt:50)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:215)
at androidx.navigation.compose.NavBackStackEntryProviderKt.LocalOwnersProvider(NavBackStackEntryProvider.kt:46)
at androidx.navigation.compose.NavHostKt$NavHost$3.invoke(NavHost.kt:132)
at androidx.navigation.compose.NavHostKt$NavHost$3.invoke(NavHost.kt:131)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.animation.CrossfadeKt$Crossfade$1$1.invoke(Crossfade.kt:74)
at androidx.compose.animation.CrossfadeKt$Crossfade$1$1.invoke(Crossfade.kt:69)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.animation.CrossfadeKt.Crossfade(Crossfade.kt:86)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:131)
at androidx.navigation.compose.NavHostKt$NavHost$4.invoke(Unknown Source:13)
at androidx.navigation.compose.NavHostKt$NavHost$4.invoke(Unknown Source:10)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2156)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2399)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2580)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2573)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(SnapshotState.kt:540)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2566)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2542)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:613)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:764)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:103)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:447)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:416)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:34)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
2021-08-21 17:45:08.153 26668-26668/com.zjuqsc.mobile4 E/AndroidRuntime: at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:727)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7660)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
I use debug breakpoint to see what happened, and it turns out that it use getViewModelStore when NavBackStackEntry reach Lifecycle.State.DESTROYED state, and I have no idea about how to fix it. I would be very grateful if anyone could help me

update to 2.4.0-alpha07 fixes my problem

Try to initialise your viewmodel in the main activity like val viewModel by viewModels<MainViewModel>()

Related

Am I Doing Jetpack Compose Navigation Right?

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)
}
)
}
}

Jetpack Compose How to pass arguments between screens with Hilt ViewModel using savedStateHandle?

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()
}
}

How to setup android navigation on jetpack compose using hilt with view models responsible for navigation?

I am getting this error when trying to navigate to another screen from the view model,
kotlin.UninitializedPropertyAccessException: lateinit property _navController has not been initialized
This is my activity code,
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AssessmentAppTheme {
// A surface container using the 'background' color from the theme
Column(modifier = Modifier
.fillMaxWidth(1f)
.padding(vertical = 10.dp, horizontal = 10.dp), horizontalAlignment = Alignment.CenterHorizontally) {
AssessmentApp(modifier = Modifier.padding(bottom = 40.dp))
NavigationGraph(navigator)
}
}
}
}
}
This is my navigation module,
#Module
#InstallIn(ActivityRetainedComponent::class)
class AppModule {
#Provides
fun providesNavigation() = Navigator()
}
This is my navigation class,
#ActivityRetainedScoped
class Navigator {
private lateinit var _navController: NavHostController
fun navigate(destination: NavigationDestination) {
_navController.navigate(destination.route)
}
fun setController(controller: NavHostController) {
_navController = controller
}
}
this is the navigation graph where I am remembering the navController,
#Composable
fun NavigationGraph(
navigator: Navigator
) {
val navController = rememberNavController()
navigator.setController(navController)
NavHost(navController = navController, startDestination = Routes.CLIENTS_ROUTE ) {
composable(Routes.CLIENTS_ROUTE) {
val viewModel = hiltViewModel<ClientViewModel>()
ClientScreen(viewModel = viewModel)
}
composable(Routes.ASSESSMENT_OPTIONS_ROUTE, arguments = listOf(navArgument(RouteArgs.CLIENT_ID) {type = NavType.StringType})) { backStackEntry ->
val viewModel = hiltViewModel<ClientViewModel>()
ClientAssessmentOptionScreen(viewModel = viewModel)
}
}
finally, this is one of view models trying to navigate to different screen,
#HiltViewModel
class ClientViewModel #Inject constructor(
private val repository: IClientRepository,
private val navigator: Navigator,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
// Some code here //
fun onEvent(event: ClientEvent) {
viewModelScope.launch {
when(event) {
is ClientEvent.OnClientClicked -> {
event.client.clientName?.let {
navigator.navigate(
NavigationDestination(Routes.generateAssessmentOptionsRoute(clientId = it))
)
}
}
}
}
}
}
What am I doing wrong here? and is the approach to make view models handle navigation the right one for jetpack compose applications?
Just answering it here in case someone else also stumbles upon this. I have modified my navigator class and added a shared flow. Which would be used sort of as an event emitter. Whenever we would want to navigate to another screen we can use the navigate method which would emit the route destination.
#Singleton
class Navigator {
private val _sharedFlow =
MutableSharedFlow<NavigationDestination>(extraBufferCapacity = 1)
val sharedFlow = _sharedFlow.asSharedFlow()
fun navigate(destination: NavigationDestination) {
_sharedFlow.tryEmit(destination)
}
}
Now in the NavigationGraph, I have remembered the NavController and have also added a launchedEffect coroutine, which would be listening to the navigate events from the flow. For each flow event, we will trigger the NavController to navigate to that emitted destination.
#Composable
fun NavigationGraph(
navController: NavHostController = rememberNavController(),
navigator: Navigator
) {
LaunchedEffect("navigation") {
navigator.sharedFlow.onEach {
navController.navigate(it.route)
}.launchIn(this)
}
NavHost(navController = navController, startDestination = Routes.CLIENTS_ROUTE ) {
// some code here... //
}
}

Create instance of a class in the hilt in the another module

I have 5 modules (main, splash, pick location, permissions, preferences).
I want to use the Preferences module in Splash and PickLocation.
This is the flow of my modules: Splash -> PickLocation
When I want to use the DataStore class in the PickLocation module to save new locations, I have a problem:
If I create a new instance of the DataStore class in the Splash module and create another instance of the same class in the PickLocation module DataStore not working, but if i just create an instance in the PickLocation everything is working. how can I use dagger hilt to create one instance and access from all modules?
Preference module DI:
package com.mykuyaclient.preference.di
import android.content.Context
import com.mykuyaclient.preference.datastores.CheckLocationIsSetDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
#Module
#InstallIn(SingletonComponent::class)
object PreferenceModule {
#Provides
fun provideCheckLocationIsSetDataStore(#ApplicationContext appContext: Context): CheckLocationIsSetDataStore =
CheckLocationIsSetDataStore(appContext)
}
PickLocationScreen codes:
#Composable
fun MapScreen(navigatorViewModel: PickLocationViewModel) {
**val context = LocalContext.current
val dataStore = CheckLocationIsSetDataStore(context = context)**
Surface(color = AppColor.ThemeColor.BACKGROUND) {
val mapView = rememberMapViewWithLifecycle()
Column(Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize()) {
MapViewContainer(mapView, navigatorViewModel)
MapPinOverlay()
Column(modifier = Modifier.align(Alignment.BottomCenter)) {
Button(
colors = ButtonDefaults.buttonColors(
backgroundColor = AppColor.brandColor.BLUE_DE_FRANCE,
contentColor = AppColor.neutralColor.DOCTOR
),
onClick = {
navigatorViewModel.apply {
// popBackStack()
navigate(HomeDestination.route())
**viewModelScope.launch {
dataStore.set(
this#apply.location.value
)
}**
}
}) {
Text(
text = stringResource(R.string.confirm_address),
style = AppFont.PoppinsTypography.button
)
}
Spacer(modifier = Modifier.size(16.dp))
}
}
}
}
}
SplashScreen codes:
#OptIn(ExperimentalPermissionsApi::class)
#Composable
private fun SplashView(
modifier: Modifier,
multiplePermissionsState: MultiplePermissionsState,
navigator: SplashViewModel = hiltViewModel()
) {
**val context = LocalContext.current
val dataStore = CheckLocationIsSetDataStore(context = context)**
Box(
modifier = modifier
.fillMaxSize()
.background(color = AppColor.brandColor.BLUE_DE_FRANCE)
.padding(start = 64.dp, end = 64.dp, bottom = 16.dp)
) {
Column(modifier = modifier.align(Alignment.Center)) {
Image(
painter = painterResource(id = R.drawable.mykuyatm),
contentDescription = "mykuya tm image"
)
Image(
painter = painterResource(id = R.drawable.mykuya_powered_by),
contentDescription = "mykuya powered by image"
)
}
Loading(modifier = modifier.align(Alignment.BottomCenter))
FeatureThatRequiresPermission(
multiplePermissionsState = multiplePermissionsState, permissionsState = {
if (it) {
navigator.apply {
viewModelScope.launch {
delay(Constants.SPLASH_DELAY)
**dataStore.get.collect { model ->
model.let {
/* if (model.lat == Constants.IF_LOCATION_LAT_NOT_SET && model.lat == Constants.IF_LOCATION_LNG_NOT_SET) {
navigate(PickLocationDestination.route())
}else{
navigate(HomeDestination.route())
}*/
navigate(PickLocationDestination.route())
}
}**
// popBackStack()
}
}
}
})
}
}
DataStore class codes: (How can i use instance of this class in the all of modules)
class CheckLocationIsSetDataStore #Inject constructor(private val context: Context) :
IDataStore<Location, Location> {
override val get: Flow<Location>
get() = context.dataStore.data.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
Log.e("DataStore Exception: ", exception.toString())
}.map { preferences ->
Location("").let {
it.latitude = preferences[DataStoreKeys.IS_LOCATION_LAT_SET_KEY]
?: Constants.IF_LOCATION_LAT_NOT_SET
it.longitude = preferences[DataStoreKeys.IS_LOCATION_LNG_SET_KEY]
?: Constants.IF_LOCATION_LNG_NOT_SET
it
}
}
override suspend fun set(param: Location?) {
context.dataStore.edit { preferences ->
preferences[DataStoreKeys.IS_LOCATION_LAT_SET_KEY] =
param?.latitude ?: Constants.IF_LOCATION_LAT_NOT_SET
preferences[DataStoreKeys.IS_LOCATION_LNG_SET_KEY] =
param?.longitude ?: Constants.IF_LOCATION_LNG_NOT_SET
}
}
}
Hilt can inject dependencies in view models, so you need to create such a model.
Here is a basic example:
class CheckLocationIsSetDataStore #Inject constructor(
#ApplicationContext val context: Context
) {
fun dataStore() = context.dataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "composePreferences")
}
#HiltViewModel
class DataStoreProviderViewModel #Inject constructor(
private val checkLocationIsSetDataStore: CheckLocationIsSetDataStore,
): ViewModel() {
private val key = booleanPreferencesKey("some_test_key")
val get get() = checkLocationIsSetDataStore.dataStore().data.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
Log.e("DataStore Exception: ", exception.toString())
}.map { preferences ->
preferences[key] ?: false
}
fun set(value: Boolean) {
viewModelScope.launch {
checkLocationIsSetDataStore.dataStore().edit {
it[key] = value
}
}
}
}
#Composable
fun TestScreen(
) {
val viewModel = hiltViewModel<DataStoreProviderViewModel>()
val some by viewModel.get.collectAsState(initial = false)
Switch(checked = some, onCheckedChange = { viewModel.set(it) })
}

Compose Navigation - navigation destination ... is not a direct child of this NavGraph

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()
}
}

Categories

Resources