Assume there is a FilterActivity with Switch and EditText controls. The latter has input disabled but is clickable. A click on it launches TypeActivity to pick a type value from and then gets the type name populated into the EditText.
There is a FilterViewModel with a StateFlow<FilterUiState>
data class FilterUiState(
var status: Boolean = false,
var type: String = "sometype"
)
which is collected by the activity in onCreate like this
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
binding.run {
statusSwitch.isChecked = it.status
typeEditText.text = it.type
}
}
}
}
The problem is if the user changes the statusSwitch state and then clicks on the typeEditText to pick a type, then on return back the FilterActivity gets resumed which triggers the uiState.collect and consequently resets the statusSwitch check state back to the initial value.
What is the right way to prevent UI changes from being overridden by StateFlow collect?
You need to actually update the state flow with the latest state you want it to hold, and then it will properly restore the latest state when collected.
Also, don't use a mutable class for this. It's error prone to mix mutable classes with StateFlows. The StateFlow cannot detect that a change has occurred if you mutate the instance it is already holding.
data class FilterUiState(
val status: Boolean = false,
val type: String = "sometype"
)
// in ViewModel class:
private val mutableUiState = with(PreferenceManager.getDefaultSharedPreferences(context)) {
val status = //...get these from shared preferences
val type = //...
val initialState = FilterUiState(status, type)
MutableStateFlow(initialState)
}
val uiState = mutableUiState.asStateFlow()
fun updateStatus(status: Boolean) {
mutableUiState.value = mutableUiState.value.copy(status = status)
// and update the SharedPreferences value
}
fun updateType(type: String) {
mutableUiState.value = mutableUiState.value.copy(type = type)
// and update the SharedPreferences value
}
And call these two update functions from the Activity when these values change. You can add a click listener to the checkbox to do this for the "status" and for the EditText, since you have input disabled, you can call the updateType() function instead of directly modifying its text when returning from the other Activity. Your existing collector will update the text in the widget for you when you update the state in the view model.
Related
I have MVVM fragment with this simple requirement.
The fragment have a EntryText field, and I want to populate with a value calculated in the view model,
when the viewmodel is created I need to call a Room request and get a value, then that value should be present to the user, and the user can change that value.
Also I'm soring that value in the State Handle
var parcelaDesde = state.get<String>("parcelaDesde")?:"0"
set(value) {
field = value
state.set("parcelaDesde", value)
}
I resolved with:
*creating a public methos in viewmodel that retrieve info from Room and update local member field, also update a MutableLiveData that is observed in Fragment
fun updateParcelaDesde(default: Int) {
val num = parcelaDesde.toIntOrNull()
num?.let {
if (it == default) {
viewModelScope.launch(Dispatchers.IO) {
parcelaDesde = filialcruzaRepository.getMinNroParcela(filial.value?.id!!)?.toString()?:"0"
Log.d(TAG, "updateParcelaDesde: $parcelaDesde")
isLoadingDesde.postValue(false)
}
}
}
}
on the Fragment, I just observe the liveData and update the UI when te loading is completed.
viewModel.isLoadingDesde.observe(viewLifecycleOwner) {
binding.etParcelaDesde.setText( viewModel.parcelaDesde.trim() )
}
Is this the correct way ?
How to do this if I want to us dataBinding?
Best Regards
I have two classes: one is the viewModel (ShoesViewMode.ktl) to keep the data and the other is the Fragment to show the data.(ShoesList.kt )
ShoesList has a mutableList of words and I recover it from the ShoesList to show in a scrollview.
I get a new word from an EditText from a Fragment -> Click on Save button -> Pass this word through nave Args to ShoesDetails -> save it in the ShoesViewModel -> Recover it and show in the Fragment.
The problem is that every time I add a new word, the list doesn't keep the last one added. It's like if the mutableList was always recreated.
I would like to go back the screen and add a new word, and a new word and see the previous words added in the list.
How can I keep the words added previously?
ShoesViewModel.kt
class ShoesViewModel(_newShoe: String?=null): ViewModel() {
private var _shoesList = MutableLiveData<MutableList<String>>()
init {
//receives the score when the class is instanciated
_shoesList.value = mutableListOf(
"trade",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll"
)
}
val shoesList: LiveData<MutableList<String>>
get() = _shoesList
fun save (newShoe: String){
_shoesList.value?.add(newShoe)
}
ShoesList. kt // FRAGMENT to show data
val shoesListArgs by navArgs()
viewModelFactory = ShoeViewModelFactory(shoesListArgs.newShoe)
viewModel = ViewModelProvider(this, viewModelFactory).get(ShoesViewModel::class.java)
//get the view Model //pass to the variable in the xml
binding.shoesViewModel = viewModel
binding.setLifecycleOwner(this)
viewModel.save(shoesListArgs.newShoe) //save new Shoe to the List
//keeps track of shoesList. This is an OBSERVER
viewModel.shoesList.observe(viewLifecycleOwner, Observer{ shoesList ->
loadShoes(shoesList)
})
//actig to floating button
binding.buttonFloating.setOnClickListener{ view:View ->
view.findNavController().navigate(ShoesListDirections.actionShoesListToShoesDetails())
}
return binding.root
}
private fun loadShoes(list:MutableList<String>){
for(shoe in list){
val newTextViewShoe = TextView(context)
newTextViewShoe.text = shoe // add TextView to LinearLayout
binding.linearlayoutShoelist.addView(newTextViewShoe)
}
}
}
I save a new word, the Fragment changes and list shows the new word. When I go back to the screen to save a new word, it saves the new word, but the previous on disappears.
In method save You need:
fun save(newShoe: String) {
if (shoeList.value.isNullOrEmpty){
shoeList.value = mutableListOf(newShoe)
}
else {
shoesList.value = shoesList.value.add(newShoe)
}
}
Your problem is that You are trying to set data to the list rather than livedata by calling livedata.value.add(). Your value here is getValue() method, that does nothing but gives you value. If You need to update a value in livedata, then You go:
liveData.value = newValue
Whether this means setValue() method. Additionally, if You want to set data from another thread than main, use postValue():
liveData.postValue(newValue)
I think I haven't quite wrapped my head around how compose states work yet. I'm not able to trigger a recomposition when an item in the uiState changes.
I'm building an app that need notification access, so for that I'm navigating the user to the settings and after the user has granted permission they have to navigate back to the app. That's where I want to trigger the recomposition.
I have the permission check in onResume working and the variable in the uiState changes, but the recomposition doesn't get called. What am I missing here?
Composable
#Composable
private fun MainLayout(viewModel: SetupViewModel){
val uiState = viewModel.uiState.collectAsState()
SetupItem(
title = "Notification access",
summary = if(uiState.value.hasNotificationPermission) stringResource(R.string.granted) else stringResource(R.string.not_granted){}
}
SetupUiState.kt
data class SetupUiState(
var hasNotificationPermission: Boolean = false
)
I know for a fact that hasNotificationPermission gets set to true, but the summary in the SetupItem does not update. How do I accomplish that?
The problem here is that the hasNotificationPermission field is mutable (var and not val). Compose is not tracking inner fields for change. You have two options here:
Modify the SetupUiState as a whole, assuming you are using StateFlow in your ViewModel, it can look like this:
fun setHasNotificationPermission(value: Boolean) {
uiState.update { it.copy(hasNotificationPermission = value) }
}
You should also change hasNotificationPermission from var to val.
You can make use of compose's State and do something like this:
class SetupUiState(
initialHasPermission: Boolean = false
) {
var hasNotificationPermission: Boolean by mutableStateOf(initialHasPermission)
}
With this you can then simply do uiState.hasNotificationPermission = value and composition will be notified, since it's tracking State instances automatically.
I have problem working with MutableStateFlow, I cannot understand how it is working or I am mistaken somewhere. For example purpose I created simpler classes to get the idea what I am doing.
First I have data class which holds the values and controller which update values in the data class
data class ExampleUiState(
val dataFlag: Boolean = false
)
class ExampleController {
private val _exampleUiState = MutableStateFlow(ExampleUiState())
val exampleUiState = _exampleUiState.asStateFlow()
fun onChangeFlag(flag: Boolean) {
_exampleUiState.update { it.copy(dataFlag = flag) }
}
}
I am using koin, and I created Example controller singleton.
Second I am injection it in my ViewModel where I have two functions there
class ExampleViewModel(
private val exampleController: ExampleController
) : ViewModel() {
val exampleUiState = exampleController.exampleUiState.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
ExampleUiState()
)
//called second
private fun useFlagInViewModelFun() {
//here the value is not updated
exampleUiState.value.dataFlag
}
//called first from UI
fun changeValueFromUi(flag: Boolean) {
//change it from default false to true
exampleController.onChangeFlag(flag)
useFlagInViewModelFun()
}
}
The idea is when I call changeValueFromUi from some compose function, I update the value with my controller function, and after it I call other function where I want to use already updated state of data class, but I don't get the correct value.
Where I am mistaken?
Is there any time needed for onChangeFlag() to react and update the value?
Am I mistaken the way that I am trying to get the value after exampleUiState.value.dataFlag ?
In my fragment I observe dbQuestionsList field:
viewModel.dbQuestionsList.observe(viewLifecycleOwner, Observer { list ->
Log.i("a", "dbQuestionsList inside fragment = $list ")
})
In my fragment I have few buttons and depending on which one is pressed I call method on viewModel passing the string which was set as tag to the button.
viewModel.onFilterChanged(button.tag as String)
My ViewMode:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("")
}
private fun onFilterChanged(filter: String) {
dbQuestionsList = mRepository.getDbQuestionsForCategory(filter)
}
Repository method:
fun getDbQuestionsForCategory(categoryName: String): LiveData<List<DatabaseQuestion>> {
return database.dbQuestionsDao().getDbQuestionsByCategory(categoryName)
}
Dao method:
#Query("SELECT * FROM db_questions_database WHERE category = :categoryId")
fun getDbQuestionsByCategory(categoryId: String): LiveData<List<DatabaseQuestion>>
When I press button, viewModel method is called with argument which should be used to update LiveData by searching through room database, but NOTHING gets updated for no reason. Database is not empty so there is no reason to return null and not trigger observer in main Fragment.
But when I do this in my viewModel:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("travel")
}
where I hardcode parameter, the room will return list and observer in fragment will be triggered, so it works like that but doesn't work when arguments is passed when button is pressed, Please explain because this thing doesn't make sense. I tried with mutable live data, with using .setValue and .postValue but NOTHING works.
The reason you aren't getting updates is because onFilterChanged() is reassigning dbQuestionsList, not updating it. So the variable you observe initially is never actually modified.
I would probably implement this using a Transformation:
val filter = MutableLiveData<String>().apply { value = "" }
val dbQuestionsList = Transformations.switchMap(filter) {
mRepository.getDbQuestionsForCategory(filter)
}
Then in your fragment just set the filter when your button is clicked:
viewModel.filter.value = button.tag as String
Try this:
dbQuestionsList.value = mRepository.getDbQuestionsForCategory(filter)
or
dbQuestionsList.postValue(mRepository.getDbQuestionsForCategory(filter))