I declared variable like this.
var saleData: MutableLiveData<SaleData> = MutableLiveData()
data class
data class SaleData(
var saleType: Int,
var saleDetail: Int,
var salePrice: Int,
var saleRate: Int,
var printAmount: Int
)
and then, init the data
init {
saleData.value = SaleData(saleType = 1, saleDetail = 0, salePrice = 0, saleRate = 0, printAmount = 1)
}
The question is, if one of the components of the data class in SaleData changes, can I be notified of this?
I simply wrote the code as below, but there was no result value.
viewModel
fun changeData() {
saleData.value?.saleRate = 50
}
fragment - at onCreateView
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.saleData.observe(viewLifecycleOwner, Observer { saleData ->
Log.d(TAG,"value changed")
})
I can't get the Log when change the saleRate in saleData
Like this code,
saleData.value = SaleData(saleType = saleType, saleDetail = 0, salePrice = 0, saleRate = 0, printAmount = 1)
I set saleData value, it notify the value changed but I want when change the item of saleData, notify the change
Is there anything else I need to set up?
A call to saleData.value?.saleRate = 50 will not notify observers because the underlying object stored within the MutableLiveData remains unchaged. For LiveData to notify its observers, you need to assign a new object to it.
Assuming SaleData is a data class, you can make it notify it's observers by calling,
saleData.value = saleData.value?.copy(saleRate = 50)
This will notify all registered observers.
As Rafsanjani said, you need to assign an updated reference of the SaleData class to the LiveData. Once you do that, the LiveData observer will notify the changes. For better understanding, please, see the code below.
SaleDataViewModel
class SaleDataViewModel: ViewModel() {
private var _saleData: MutableLiveData<SaleData> = MutableLiveData()
val saleData: LiveData<SaleData> = _saleData
init {
_saleData.value = SaleData(saleType = 1, saleDetail = 0, salePrice = 0, saleRate = 0, printAmount = 1)
}
fun updateSale() {
val saleRate = updatedSaleRate()
_saleData.value = SaleData( _saleData.value!!.saleType, _saleData.value!!.saleDetail, _saleData.value!!.salePrice, saleRate, _saleData.value!!.printAmount)
}
private fun updatedSaleRate(): Int {
return 50
}
}
SaleDataFragment
class SaleDataFragment: Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) {
viewModel.updateSale()
viewModel.saleData.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "Notify change")
})
}
}
Related
In my SettingActivity I'm loading some data to be shown in ListPreference from Room, once the items are selected all works correctly, the value is saved to `SharedPreferences and the summary is shown correctly, but once I return to SettingsActivity the summary value is reset to null.
Here is what is happening:
My code is pretty simple, onViewCreated() I start observing LiveData to be shown in ListPreference and then I set the values for entries and entryValues
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.shops.observe(viewLifecycleOwner) {
setShopsPreferences(it)
}
}
private fun setShopsPreferences(shops: List<Shop>) {
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
if (shops.isEmpty()) {
shopsPreference?.isEnabled = false
return
} else {
shopsPreference?.isEnabled = true
}
val entries: ArrayList<String> = ArrayList()
val entryValues: ArrayList<String> = ArrayList()
shops.forEach {
entries.add(it.description)
entryValues.add(it.id)
}
shopsPreference?.entryValues = entryValues.toArray(arrayOfNulls<CharSequence>(entryValues.size))
shopsPreference?.entries = entries.toArray(arrayOfNulls<CharSequence>(entries.size))
}
ViewModel:
#HiltViewModel
class ShopsViewModel #Inject constructor(repository: ShopsRepository) : ViewModel() {
private val _shops = MutableLiveData<List<Shop>>()
val shops: LiveData<List<Shop>> = _shops
init {
repository.getAllShops().observeForever {
_shops.value = it
}
}
}
Repository:
fun getAllShops(): LiveData<List<Shop>> {
return shops.select()
}
DAO:
#Dao
interface ShopsDAO {
#Query("SELECT * FROM shops")
fun select(): LiveData<List<Shop>>
}
You are populating list entries dynamically after the preference hierarchy is already created by inflating the xml. But during that time there was no entry, hence the value was null. The data was then retrieved asynchronously but the change will not be reflected. So you have to set the summary manually.
Another approach I'm not sure about is to call recreate on the activity after populating the data inside the observer listener.
To resolve the issue with dynamic data in a ListPreference I've made some changes to my code, first of call in onCreatePreferences() I've added a preference listener to my ListPreference like this:
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
shopsPreference?.setOnPreferenceChangeListener { preference, newValue ->
val description = viewModel.shops.value?.find { it.id == newValue }
preference.summary = description?.description
true
}
}
So in that way on each new selection, I'm looking for the matching value for that preference in my ViewModel and getting the description for it which then is set as the summary.
While to set the summary every time the list has been changed or the SettingsActivity is opened I've added the following code to my setShopsPreferences() function:
private fun setShopsPreferences(shops: List<Shop>?) {
if (shops == null) {
return
}
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
// The preference is enabled only if there are shows inside the list
shopsPreference?.isEnabled = shops.isNotEmpty()
// Getting the defaultShop preference value, which will be used to find the actual shop description
val defaultShop = sharedPreferences.getString("defaultShop", "0")
defaultShop?.let { shopId ->
// Setting the summary based on defaultShop saved preference
shopsPreference?.summary = shops.find { it.id == shopId }?.description
}
val entries: ArrayList<String> = ArrayList()
val entryValues: ArrayList<String> = ArrayList()
shops.forEach {
entries.add(it.description)
entryValues.add(it.id)
}
shopsPreference?.entryValues =
entryValues.toArray(arrayOfNulls<CharSequence>(entryValues.size))
shopsPreference?.entries = entries.toArray(arrayOfNulls<CharSequence>(entries.size))
}
in my ViewModel:
private val _itemList = mutableStateListOf<Post>()
val itemList: List<Post> = _itemList
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy(isLiked = true)
}
Here my Post data class:
data class Post(
val id: Int,
val name: String,
val isLiked: Boolean = false,
)
And here my Composable:
val postList = viewModel.itemList
LazyRow(content = {
items(postList.size) { i ->
val postItem = postList[i]
PostItem(
name = postItem.name,
isLiked = postItem.isLiked,
likePost = { viewModel.likePost(postItem)}
)
}
})
The change does not update in the UI instantly, I first have to scroll the updated item out of the screen so it recomposes or switch to another Screen and go back to see the change.
For some reason it doesn't like updating, it will add and delete and update instantly. You have to do it this way when updating for our to update the state.
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy()
_itemList[index].isLiked = true
}
You are returning a List<> effectively and not MutableStateList from your ViewModel.
If you want the list to not be mutable from the view, I happen to use MutableStateFlow<List<>> and return StateFlow<List<>>. You could also just convert it to a list in your composable.
Edit:
//backing cached list, or could be data source like database
private val deviceList = mutableListOf<Device>()
private val _deviceListState = MutableStateFlow<List<Device>>(emptyList())
val deviceListState: StateFlow<List<BluetoothDevice>> = _deviceListState
//manipulate and publish
fun doSomething() {
_deviceListState.value = deviceList.filter ...
}
In your UI
val deviceListState = viewModel.deviceListState.collectAsState().value
Below is my viewmodel class body
private var _movieState = mutableStateOf(false)
val movieState = _movieState
private val query = if (_movieState.value) ListState.POPULAR_PLAYING else ListState.ALL_PLAYING
val moviesData: Flow<PagingData<Movie>> = Pager(PagingConfig(pageSize = 10)) {
MoviePagingSource(movieRepository, query.string)
}.flow
And I have Two function which populate the Boolean value
fun setListToPopular(){
_movieState.value = true
}
fun setListToAllNowPlaying(){
_movieState.value = false
}
which will be triger from UI
Now i have one mistake once the code of viewmodel runs my if block is over but i want to observe that Boolean state within it.
how should i do that ?
I have a screen with several switchcompats like on photo.
And I want to collect only last input of each of them to send it to server. I use SharedFlow. Now I take last state by debounce but it returns only one for all the toggles. How can I use debounce or other function on Flow so as to collect last state of each of the toggles? I have a unique field in Toggle class to group by.
You can create a data class to hold all the state of the switches. And update the state this data class by the switch's id.
data class SwitchesState(
val switch1:Boolean = false,
val switch2:Boolean = false,
val switch3:Boolean = false,
val switch4:Boolean = false,
val switch5:Boolean = false
)
class SwitchesViewModel : ViewModel {
private val _switchesFlow = MutableStateFlow(SwitchesState());
val switchesFlow:Flow get() = _switchesFlow;
fun updateSwitch(id:Int, state:Boolean) {
_switchesFlow.value = when(id) {
R.id.switch1 -> _switchesFlow.value.copy(swith1 = state)
R.id.switch2 -> _switchesFlow.value.copy(swith2 = state)
R.id.switch3 -> _switchesFlow.value.copy(swith3 = state)
R.id.switch4 -> _switchesFlow.value.copy(swith4 = state)
else -> _switchesFlow.value.copy(swith5 = state)
}
}
}
class SwitchesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState:Bundle) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.switchesFlow.debounce(500).collect {
// Do something
}
}
}
}
I'm trying to display a 4x4 grid with values that change depending on user input. To achieve that, I created mutableStateListOf that I use in a ViewModel to survive configuration changes. However, when I try to replace a value in that particular list using button onClick, it keeps doing that until app crashes. I can't understand why is onReplaceGridContent looping after clicking the button once. Currently, my code looks like this:
ViewModel:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
fun initialize(){
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Button:
Button(
onClick = {
onReplaceGridContent(GameUtils.getRandomTileNumber(),GameUtils.getRandomTilePosition(gameGridContent))},
colors = Color.DarkGray
){
Text(text = "Add number to tile")
}
Activity Composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
gameViewModel.initialize()
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}
Your initialize will actually run on every recomposition of gameScreen:
You click on a tile - state changes causing recomposition.
initializa is called and changes the state again causing recomposition.
Step 2 happens again and again.
You should initialize your view model in its constructor instead (or use boolean flag to force one tim initialization) to make it inly once.
Simply change it to constructor:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
init {
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Now you don't need to call initialize in the composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}