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) })
}
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()
}
}
ViewModel does not preserve state during configuration changes, e.g., leaving and returning to the app when switching between background apps.
#HiltViewModel
class MainViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
) : ViewModel()
{
var title by mutableStateOf("")
internal set
var showMenu by mutableStateOf(false)
internal set
var tosVisible by mutableStateOf(false)
internal set
}
The menu:
Currently: It survives the rotation configuration change, the menu remains open if opened by clicking on the three ... dots there. But, changing app, i.e. leaving the app and going into another app. Then returning, does not preserve the state as expected. What am I possibly doing wrong here?
In MainActivity:
val mainViewModel by viewModels<MainViewModel>()
Main(mainViewModel) // Passing it here
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun Main(viewModel: MainViewModel = viewModel()) {
val context = LocalContext.current
val navController = rememberNavController()
EDIT: Modified my ViewModel to this, Makes no difference.
#HiltViewModel
class MainViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
) : ViewModel()
{
var title by mutableStateOf("")
internal set
var showMenu by mutableStateOf(savedStateHandle["MenuOpenState"] ?: false)
internal set
var tosVisible by mutableStateOf(savedStateHandle["AboutDialogState"] ?: false)
internal set
fun displayAboutDialog(){
savedStateHandle["AboutDialogState"] = tosVisible;
}
fun openMainMenu(){
savedStateHandle["MenuOpenState"] = showMenu;
}
}
Full code of the MainActivity:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun Main(viewModel: MainViewModel) {
val context = LocalContext.current
val navController = rememberNavController()
//val scope = rememberCoroutineScope()
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
decayAnimationSpec,
rememberTopAppBarScrollState()
)
LaunchedEffect(navController){
navController.currentBackStackEntryFlow.collect{backStackEntry ->
Log.d("App", backStackEntry.destination.route.toString())
viewModel.title = getTitleByRoute(context, backStackEntry.destination.route);
}
}
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
viewModel.title,
//color = Color(0xFF1877F2),
style = MaterialTheme.typography.headlineSmall,
)
},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.background
),
actions = {
IconButton(
onClick = {
viewModel.showMenu = !viewModel.showMenu
}) {
Icon(imageVector = Icons.Outlined.MoreVert, contentDescription = "")
MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(20.dp))) {
IconButton(
onClick = { viewModel.showMenu = !viewModel.showMenu }) {
Icon(imageVector = Icons.Outlined.MoreVert, contentDescription = "")
DropdownMenu(
expanded = viewModel.showMenu,
onDismissRequest = { viewModel.showMenu = false },
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(0.dp),
properties = PopupProperties(focusable = true)
) {
DropdownMenuItem(text = { Text("Sign out", fontSize = 16.sp) }, onClick = { viewModel.showMenu = false })
DropdownMenuItem(text = { Text("Settings", fontSize = 16.sp) }, onClick = { viewModel.showMenu = false })
Divider(color = Color.LightGray, thickness = 1.dp)
DropdownMenuItem(text = { Text("About", fontSize = 16.sp) },
onClick = {
viewModel.showMenu = true
viewModel.tosVisible = true
})
}
}
}
}
},
scrollBehavior = scrollBehavior
) },
bottomBar = { BottomAppBar(navHostController = navController) }
) { innerPadding ->
Box(modifier = Modifier.padding(PaddingValues(0.dp, innerPadding.calculateTopPadding(), 0.dp, innerPadding.calculateBottomPadding()))) {
BottomNavigationGraph(navController = navController)
}
}
}
Solved with this:
#HiltViewModel
class MainViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
) : ViewModel()
{
companion object {
const val UI_MENU_STATE = "ui.menu.state"
}
init {
savedStateHandle.get<Boolean>(UI_MENU_STATE)?.let {
m -> onMenuStateChange(m);
}
}
private var m_title by mutableStateOf("")
private var displayMenu by mutableStateOf( false)
var tosVisible by mutableStateOf( false)
internal set
fun updateTitleState(title: String){
m_title = title;
}
fun onMenuStateChange(open: Boolean){
Log.d("App", open.toString());
displayMenu = open;
savedStateHandle.set<Boolean>(UI_MENU_STATE, displayMenu);
}
fun isMenuOpen(): Boolean {
return displayMenu;
}
fun getTitle(): String { return m_title; }
}
Maybe you are trying to access another instance of the view model or creating a new instance of the view model upon re-opening the app. The code should work fine if you are accessing the same instance.
Edit:
Seems like you were in fact reinitializing the ViewModel. You are creating a new instance of ViewModel by putting the ViewModel inside the constructor because when you re-open the app the composable will be created again, thus creating a new ViewModel instance. What I will suggest you do is initialise the ViewModel inside composable like this maybe:
fun Main(){
val viewModel = ViewModelProvider(this#MainActivity)[MainViewModel::class.java]
}
The following code is from the official Advanced State in Jetpack Compose Codelab.
In the function fun DetailsScreen(), uiState.isLoading -> {...} will be fired when isLoading is true.
I searched all the code in the project, I can only find the code val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) to pass value to isLoading.
Will uiState.isLoading -> {...} always be fired in the project?
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
No
The default value of isLoading is false:
val isLoading: Boolean = false,
So the constructor calls that don't explicitly set it to true (DetailsUiState(throwError = true) and DetailsUiState(throwError = true)) will result in it being false.