I am creating an app with MVVM architecture and I ran into an issue of getting a list of LiveData to show in the View.
In my ViewModel I have a getAll() function that retrieves a list of strings from the database using Room. From there, I get the strings and call my Retrofit function to send each string individually to a web-server that returns an object. Here is where my issue occurs.
From the MVVM tutorials I see online, they usually have the LiveData> style but in this since I am getting each object individually, it becomes List> but I don't think this is the correct way of doing it because in my View I would need to do a ForEach loop to observe each LiveData object in the list.
I have tried other work arounds but it doesn't seem to work. Is there a better way of doing this?
DAO
#Query("SELECT * FROM table")
fun getAll(): LiveData<List<String>>
Repository
fun getAll(): LiveData<List<String>> {
return dao.getAll()
}
fun getRetrofitObject(s: String): LiveData<RetrofitObject> {
api = jsonApi.getRetrofitObjectInfo(s, API_KEY)
val retrofitObject: MutableLiveData<RetrofitObject> = MutableLiveData()
api.enqueue(object : Callback<RetrofitObject> {
override fun onFailure(call: Call<RetrofitObject>?, t: Throwable?) {
Log.d("TEST", "Code: " + t.toString())
}
override fun onResponse(call: Call<RetrofitObject>?, response: Response<RetrofitObject>?) {
if (response!!.isSuccessful) {
retrofitObject.value = response.body()
}
}
})
return retrofitObject
}
MainActivityViewModel (ViewModel)
var objectList ArrayList<LiveData<retrofitObject>> = ArrayList()
// This is getting objects using Retrofit
fun getRetrofitObject(s: String): LiveData<retrofitObject> {
return repo.getRetrofitObject(s)
}
// This is getting all the strings from the internal database
fun getAll(): ArrayList<LiveData<retroFitObject>> {
repo.getAll().value?.forEach {it ->
objectList.add(getRetrofitObject(it)) //How else would I be able to do this?
}
return objectList
}
MainActivity (View)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityViewModel.getAll().forEach {
it.observe(this, Observer {it ->
mainActivityViewModel.objectList.add(it) //Here is part of the issue since I don't want to use a forloop in the View
})
}
adapter.objectList = mainActivityViewModel.objectList
recyclerView.adapter = adapter
}
Thanks, let me know if there is anything else needed or confusion!
By looking at the above code you are trying to fetch a list of item for each table row from server and and trying to update the result to your recycler view. Your logic is little confusing..
So on your activity .. first init your adapter and recyclerview
Then call your viewmodel function to get all values inside table and make a loop to call your network thread fuction in background and store the values in a live data object.
Just observe this livedata in your activity/fragment and just pass the list to adapter and notify it.by doing this,whenever your livedata got a change your recyclerview also reflect the items
The problem with your code is, you are called a retrofit network function with enque option and its a background thread process.so, code wont wait for the network completion. And it will return the retrofitObject data.but it has not got the data yet.so this will make error.
There might be other methods exist I don't know about them.
But you can deal with situation using Transformations for more information please look at documentation page.
Transformations.switchMap(LiveData trigger, Function> func)
You don't have to put the live data observer inside for loop.
Related
I have this MutableStateFlow<>() declaration:
private val _books = MutableStateFlow<List<Book>>(emptyList())
I am trying to append/add results from the database:
fun fetchAllBooks(user_id: Long) = viewModelScope.launch(Dispatchers.IO) {
dbRepository.getAllUsersBooks(user_id).collect{ books ->
_books.add() // Does not exist, nor does the 'postValue' method exists
}
}
But, this does not work as I though, non of the expected methods exists.
If you need to update the state of a MutableStateFlow, you can set the value property:
fun fetchAllBooks(user_id: Long) = viewModelScope.launch(Dispatchers.IO) {
dbRepository.getAllUsersBooks(user_id).collect{ books ->
_books.value = books
}
}
It will trigger events on collectors if the new value is different from the previous one.
But if getAllUsersBooks already returns a Flow<List<Book>>, you could also simply use it directly instead of updating a state flow.
If you really want a StateFlow, you can also use stateIn:
fun fetchAllBooks(user_id: Long) = dbRepository.getAllUsersBooks(user_id)
.flowOn(Dispatchers.IO) // likely unnecessary if your DB has its own dispatcher anyway
.stateIn(viewModelScope)
You are actually declaring the immutable list and trying to add and remove data instade of that use mutable list to add or remove data from list like here :-
private var _bookState = MutableStateFlow<MutableList<Book>>(mutableListOf())
private var books=_bookState.asStateFlow()
var bookList= books.value
and to send the data to the state use this:-
viewModelScope.launch {
_bookState.value.add(BookItem)
}
viewModelScope.launch {
_bookState.value.remove(BookItem)
}
I hope this will work out for you if you have any query pls tell me in comment.
Right now, my method of updating my jetpack compose UI on database update is like this:
My Room database holds Player instances (or whatever they're called). This is my PlayerDao:
#Dao
interface PlayerDao {
#Query("SELECT * FROM player")
fun getAll(): Flow<List<Player>>
#Insert
fun insert(player: Player)
#Insert
fun insertAll(vararg players: Player)
#Delete
fun delete(player: Player)
#Query("DELETE FROM player WHERE uid = :uid")
fun delete(uid: Int)
#Query("UPDATE player SET name=:newName where uid=:uid")
fun editName(uid: Int, newName: String)
}
And this is my Player Entity:
#Entity
data class Player(
#PrimaryKey(autoGenerate = true) val uid: Int = 0,
#ColumnInfo(name = "name") val name: String,
)
Lastly, this is my ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getDatabase(application)
val playerNames = mutableStateListOf<MutableState<String>>()
val playerIds = mutableStateListOf<MutableState<Int>>()
init {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().getAll().collect {
playerNames.clear()
playerIds.clear()
it.forEach { player ->
playerNames.add(mutableStateOf(player.name))
playerIds.add(mutableStateOf(player.uid))
}
}
}
}
fun addPlayer(name: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().insert(Player(name = name))
}
}
fun editPlayer(uid: Int, newName: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().editName(uid, newName)
}
}
}
As you can see, in my ViewHolder init block, I 'attach' a 'collector' (sorry for my lack of proper terminology) and basically whenever the database emits a new List<Player> from the Flow, I re-populate this playerNames list with new MutableStates of Strings and the playerIds list with MutableStates of Ints. I do this because then Jetpack Compose gets notified immediately when something changes. Is this really the only good way to go? What I'm trying to achieve is that whenever a change in the player table occurs, the list of players in the UI of the app gets updated immediately. And also, I would like to access the data about the players without always making new requests to the database. I would like to have a list of Players at my disposal at all times that I know is updated as soon as the database gets updated. How is this achieved in Android app production?
you can instead use live data. for eg -
val playerNames:Livedata<ListOf<Player>> = db.playerDao.getAll().asliveData
then you can set an observer like -
viewModel.playerNames.observe(this.viewLifecycleOwner){
//do stuff when value changes. the 'it' will be the changed list.
}
and if you have to have seperate lists, you could add a dao method for that and have two observers too. That might be way more efficient than having a single function and then seperating them into two different lists.
First of all, place a LiveData inside your data layer (usually ViewModel) like this
val playerNamesLiveData: LiveData<List<Player>>
get() = playerNamesMutableLiveData
private val playerNamesMutableLiveData = MutableLiveData<List<Player>>
So, now you can put your list of players to an observable place by using playerNamesLiveData.postValue(...).
The next step is to create an observer in your UI layer(fragment). The observer determines whether the information is posted to LiveData object and reacts the way you describe it.
private fun observeData() {
viewModel.playerNamesLiveData.observe(
viewLifecycleOwner,
{ // action you want your UI to perform }
)
}
And the last step is to call the observeData function before the actual data posting happens. I prefer doing this inside onViewCreated() callback.
it is a known issue that ListAdapter (actually the AsyncListDiffer from its implementation) does not update the list if the new list only has modified items but has the same instance. The updates do not work on new instance list either if you use the same objects inside.
For all of this to work, you have to create a hard copy of the entire list and objects inside.
Easiest way to achieve this:
items.toMutableList().map { it.copy() }
But I am facing a rather weird issue. I have a parse function in my ViewModel that finally posts the items.toMutableList().map { it.copy() } to the LiveData and gets observes in the fragment. Even with the hard copy, DiffUtil does not work. If I move the hard copy inside the fragment, then it works.
To get this easier, if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items)
... then, it doesn't work. But if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items.toMutableList().map { it.copy() })
... then it works.
Can anybody explain why this doesn't work?
In the mean time, I have opened an issue on the Google Issue Tracker because maybe they will fix the AsyncListDiffer not updating same instance lists or items. It defeats the purpose of the new adapter. The AsyncListDiffer SHOULD ALWAYS accept same instance lists or items, and fully update using the diff logic that the user customises in the adapter.
I made a quick sample using DiffUtil.Callback and ListAdapter<T, K> (so I called submitList(...) on the adapter), and had no issues.
Then I modified the adapter to be a normal RecyclerView.Adapter and constructed an AsyncDiffUtil inside of it (using the same DiffUtil.Callback from above).
The architecture is:
Activity -> Fragment (contains RecyclerView).
Adapter
ViewModel
"Fake Repository" that simply holds a val source: MutableList<Thing> = mutableListOf()
Model
I've created a Thing object: data class Thing(val name: String = "", val age: Int = 0).
For readability I added typealias Things = List<Thing> (less typing). ;)
Repository
It's fake in the sense that items are created like:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
But the "source" is a mutableList of (the typealias).
The other thing the repo can do is "simulate" a modification on a random item. I simply create a new data class instance, since it's obviously all immutable data types (as they should be). Remember this is just simulating a real change that may have come from an API or DB.
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
ViewModel
Nothing fancy here, it talks and holds the reference to the ThingsRepository, and exposes a LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
And the "state" is:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
The viewModel has two public methods (Aside from the val state):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
Nothing special, just a way to modify a random item by position (remember this is just a quick hack to test it).
So FetchData, launches the async code in IO to "fetch" (in reality, if the list is there, the cached list is returned, only the 1st time the data is "made" in the repo).
Modify data is simpler, calls modify on the repo and fetch data to post the new value.
Adapter
Lots of boilerplate... but as discussed, it's just an Adapter:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback is just:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
This Adapter now has an AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this in this context is the actual adapter (needed by the differ) and DiffUtilCallback is just a DiffUtil.Callback implementation:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
nothing special here.
The only special methods in the adapter (aside from onCreateViewHolder and onBindViewHolder) are these:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
So we ask the differ to do these for us and expose the public method submitList to emulate a listAdapter#submitList(...), except we delegate to the differ.
Because you may be wondering, here's the ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
Don't be too harsh, I know i passed the click listener directly, I only had about 1 hour to do all this, but nothing special, the layout it's just two text views (age and name) and we set the whole row clickable to pass the position to the callback. Nothing special here either.
Last but not least, the Fragment.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
It has 3 member variables. The ViewModel, the Binding (I used ViewBinding why not it's just 1 liner in gradle), and the Adapter (which takes the Click listener in the ctor for convenience).
In this impl., I simply call the viewmodel with "modify item at position (X)" where X = the position of the item clicked in the adapter. (I know this could be better abstracted but this is irrelevant here).
there's only two other implemented methods in this fragment...
onDestroy:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
Anyway, the other is unsurprisingly, onCreateView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
Bind the thing (root/binding), get the viewModel, observe the "state", set the adapter in the recyclerView, and call the viewModel to start fetching data.
That's all.
How does it work then?
The app starts, the fragment is created, subscribes to the VM state LiveData, and triggers the Fetch of data.
The ViewModel calls the repo, which is empty (new), so makeItems is called the list now has items and cached in the repo's "source" list. The viewModel receives this list asynchronously (in a coroutine) and posts the LiveData state.
The fragment receives the state and posts (submit) to the Adapter to finally show something.
When you "click" on an Item, ViewHolder (which has a click listener) triggers the "call back" towards the fragment which receives a position, this is then passed onto the Viewmodel and here the data is mutated in the Repo, which again, pushes the same list, but with a different reference on the clicked item that was modified. This causes the ViewModel to push a new LIveData state with the same list reference as before, towards the fragment, which -again- receives this, and does adapter.submitList(...).
The Adapter asynchronously calculates this and the UI updates.
It works, I can put all this in GitHub if you want to have fun, but my point is, while the concerns about the AsyncDiffer are valid (and may be or been true), this doesn't seem to be my (super limited) experience.
Are you using this differently?
When I tap on any row, the change is propagated from the Repository
UPDATE: forgot to include the doNothing function:
val doNothing: Unit
get() = Unit
I've used this for a while, I normally use it because it reads better than XXX -> {} to me. :)
While doing
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
you are creating a new list but items remains the same. You have to store that new list into a variable or passing that operation directly as a param to postItem.
I went threw plenty of similar posts on SO, but still I was not able to find the reason why I am not able to observe the LiveData on the UI.
I am working on a project for learning purposes. What I am trying to do is to fetch a value from my Roomdatabase and display it on the UI. For the sake of clearness, I am showing only the relevant code snippets.
Dao.kt
#Query("SELECT maxValue FROM DoItAgainEntity WHERE id = :id")
fun getMaxValue(id: Int): LiveData<Int>
Repository.kt
override fun getMaxValue(activityId: Int) = doItAgainDao.getMaxValue(activityId)
ViewModel.kt
private val mMaxValue = MutableLiveData<Int>()
override fun getMaxValue(id: Int): LiveData<Int> =
Transformations.switchMap(mMaxValue) {
repository.getMaxValue(id)
}
Fragment.kt
viewModel.getMaxValue(activitiesAdapter.activities[position].id)
viewModel.getMaxValue(activitiesAdapter.activities[position].id).observe(viewLifecycleOwner, Observer {
// THIS CODE IS NEVER CALLED
Log.d("getMaxValueObserver",it.toString())
}
How can I reach the Int value provided in the LiveData object? I know, this is almost a newbie question, but could you please explain me what I am missunderstanding?
Thanks a lot.
You do not get your LiveData updated because you do switchMap on mMaxValue. But that LiveData never changes (as I can see from your sample).
You actually do not need the property private val mMaxValue = MutableLiveData<Int>().
You can simplify your function to:
fun getMaxValue(id: Int): LiveData<Int> = repository.getMaxValue(id)
SwitchMap
Documentation
Returns a LiveData mapped from the input source LiveData by applying switchMapFunction to each value set on source.
There were a couple of steps that needed to be done to make this code working. Many thanks to #ChristianB for solving this problem. These were his suggestions:
If the Entity has more than one column/field, how should Room know which value you want when LiveData<Int> is returned? So, the whole row (which is definded as an Entity object) must be returned instead, this would be the entity where the ID matches the parameter :id.
Dao.kt
#Query("SELECT * FROM DoItAgainEntity WHERE id = :id")
fun getMaxValue(id: Int): LiveData<DoItAgainEntity>
in the Repository you can filter/map for any field you actually need. Or even map it to a complete different (domain) object.
Repository.kt
override fun getMaxValue(activityId: Int): LiveData<Int> = doItAgainDao.getMaxValue(activityId).map { entity -> entity.maxValue }
The switchMap(someLiveData) gets only called when the value of someLiveData changes, and then another liveData can be returned from it. But this was not my case. I just wanted to get a value from the LiveData, which goes down to my DAO. So switchMap is not needed here.
ViewModel.kt
override fun getMaxValue(id: Int) = repository.getMaxValue(id)
Finally, observing the LiveData on the UI works. The duplicate call to viewModel.getMaxValue(activitiesAdapter.activities[position].id) from the original question should be also removed.
Fragment.kt
viewModel.getMaxValue.observe(viewLifecycleOwner, Observer {
// THIS CODE HAS NOW BEEN CALLED :)
Log.d("getMaxValueObserver",it.toString())
return#Observer
})
Last but not least, If the Entity was updated like it was in my case, after creating the DB first, uninstall the app completely, do a proper DB version increase, so Room can run a migration on the DB! Clean & Rebuild the Project, Invalidate Caches / Restart.
In ViewModel
val mMaxValue = MutableLiveData<Int>()
override fun getMaxValue(id: Int){
repository.getMaxValue(id).observeForEver{it ->
mMaxValue.value = it
}
}
And in the Activity or Fragment :
viewModel.getMaxValue(activitiesAdapter.activities[position].id)
viewModel.mMaxValue.observe(viewLifecycleOwner, Observer {
// THIS CODE IS NEVER CALLED
Log.d("getMaxValueObserver",it.toString())
}
I have a fragment showing a list of items, observing from view model (from a http service, they are not persisted in database). Now, I need to delete one of those items. I have a delete result live data so the view can observe when an item has been deleted.
Fragment
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
viewModel.deleteItemLiveData.observe(viewLifecycleOwner) {
when (it.status) {
Result.Status.ERROR -> showDeletingError()
Result.Status.SUCCESS -> {
itemsAdapter.remove(it.value)
commentsAdapter.notifyItemRemoved(it.value)
}
}
}
}
fun deleteItem(itemId: String, itemIndex: Int) = lifecycleScope.launch {
viewModel.deleteItem(itemId, itemIndex)
}
ViewModel
val deleteItemLiveData = MutableLiveData<Result<Int>>()
suspend fun deleteItem(itemId: String, itemIndex: Int) = withContext(Dispatchers.IO) {
val result = service.deleteItem(itemId)
withContext(Dispatchers.Main) {
if (result.success) {
deleteItemLiveData.value = Result.success(itemIndex)
} else {
deleteItemLiveData.value = Result.error()
}
}
}
It is working fine, but the problem comes when I navigate to another fragment and go back again. deleteItemLIveData is emitted again with the last Result, so fragment tries to remove again the item from the adapter, and it crashes.
How con I solve this?
Rather than deleting an individual item from the adapter, it would make sense to update the original source of LiveData<List> since the view observes that list.
The item repository should handle deletions, removing that item from the LiveData<List> which in turns propagates the update to the view and then the adapter.
Repo might look something like this...
fun deleteItem(item: Item): Result {
val updated = items.value
updated.remove(item)
items.postValue(updated)
. . .
// propagate result of success/failure back to the view
}
fun observeItems() = items
In your fragment you would get immediate updates from a single LiveData source
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.observeItems().observe(viewLifecycleOwner) {
itemsAdapter.update(it) //use DiffUtil to update list or notifyDataSetChanged
}
}
}
Showing errors should be contextual, a toast message or some visual notification.
Update:
Handle error in deletion might look like this, off the top of my head...
suspend fun deleteItem(itemId: String, itemIndex: Int): Result = withContext(Dispatchers.IO) {
val result = service.deleteItem(itemId)
withContext(Dispatchers.Main) {
if (result.success) {
// push updated list to items
val updated = items.value
updated.remove(item)
items.postValue(updated)
Result.Success()
} else {
Result.error()
}
}
}
I found a solution. I changed my code so fragment observes from onCreate method instead of onViewCreated. And I changed the owner as well. Instead of viewLifecycleOwner now is this. This way, value is not re-emitted when fragment is resumed, but just when is created or viewModel.deleteItem is called specifically.
It is working properly now. If anybody considers this a bad solution, please, tell me.
It's a common problem when you use LiveData for events that should happen only one time. There are several solutions explained here and here. They either wrap the emitted data or the observers. In this wrapper they store a flag that tracks whether or not the event has been handled/emitted yet.