I have a collection of parent objects each having a collection of child objects. Call these ParentModels and ChildModels.
On screen I want to display a RecyclerView of rendered ParentModels, each containing inter alia a RecyclerView of rendered ChildModels.
Wishing to avoid having a god LiveData that redraws everything just because one property of one ChildModel changes, I intend to separate these.
I can't figure out how to structure this with Recyclerview Adapters and Holders plus whatever Fragments and ViewModels I need. Right now I have
class MyFragment: Fragment() {
private lateinit val mViewModel: FragmentViewModel
// ...
fun onViewCreated(/*...*/) {
val parentAdapter = ParentAdapter()
view.findViewById<RecyclerView>(/*...*/).apply {
adapter = parentAdapter
//...
}
viewModel.getParents().observe(this, Observer {
parentAdapter.setParents(it)
}
}
}
class FragmentViewModel #Inject constructor(repository: RoomRepo): ViewModel() {
mParents: LiveData<List<ParentModel>> = repository.getParents()
fun getParents() = mParents
//...
}
class ParentAdapter: RecyclerView.Adapter<ParentHolder>() {
private lateinit var mParents: List<ParentModel>
fun setParents(list: List<ParentModel>) {
mParents = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, /*...*/) {
return ParentHolder(LayoutInflater.from(parent.context).inflate(R.layout.parent, parent, false))
}
override fun onBindViewHolder(holder: ParentHolder, position: Int) {
holder.bind(/*UNKNOWN*/)
}
// ...
inner class ParentHolder(private val mView: View): RecyclerView.ViewHolder(mView) {
fun bind(/*UNKNOWN*/) {
// WHAT TO DO HERE???
}
}
}
Plus my R.layout.parent (I've omitted other irrelevant stuff like a View that just draws a horizontal line, but that's why I have my RecyclerView nested inside a LinearLayout):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
I have written a ChildAdapter, ChildHolder, and a few other things unthinkingly because I thought this would be trivial to implement, but at this point something's gunked up my brain and I'm likely not seeing the obvious thing.
I've got the first RecyclerView loading correctly based on underlying data. But this parent recyclerview also needs to:
fetch children based on a single parent.id
create a child recyclerview for a single parent recyclerview item that displays the children
Room returns a LiveData> from function repository.getChildrenByParentId(id: Long). That's the data I'm working from.
But where do I fetch this, how do I hook it into the relevant child recyclerview that belongs to the parent recyclerview?
I don't want to have a God fragment that does
viewModel.getParents().observe(...) { parentAdapter.update(it) } and also have to do some kind of viewModel.getChildren().observe(...) { parentAdapter.updateChildren(it) }
because that destroys separation of concerns. Seems to me each item in the parent recyclerview should have a viewmodel that fetches the children that would belong to it, then creates a recyclerview and uses a ChildAdapter to display these children, but I can't seem to figure out where to plug in the ChildFragment and ChildViewModel (with repository.getChildrenByParentId in it) to get this all working.
All examples I find online don't seem to help as they use contrived examples with no LiveData and a God fragment/activity that puts everything inside a single adapter.
I would literally have 1 adapter that can render everything, using the DiffUtil (or its async version) class to ensure I don't (and I quote) "redraw everything just because one property of one ChildModel changes".
I would move this complex responsibility of constructing (and providing) the data, to your repository (or, if you prefer to have it closer, to your ViewModel acting as a coordinator between 1 or more (I don't know how your model looks, so I am only imagining) repositories providing data.
This would allow you to offer to the ui a much more curated immutable list of ParentsAndChildren together and your RecyclerView/Adapter's responsibility is suddenly much simpler, display this, and bind the correct view for each row. Your UI is suddenly faster, spends much less time doing things on the main thread and you can even unit test the logic to create this list, completely independent of your Activity/Fragment.
I imagine ParentsAndChildren to be something like:
class ParentChildren(parent: Parent?, children: Children?)
Your bind could then inflate one view when parent is not null and children is. When children is not null, you know it's a children (you could include the parent as well, depends on how you construct this data). Problem solved here, your adapter would look like
class YourAdapter : ListAdapter<ParentChildren, RecyclerView.ViewHolder>(DiffUtilCallback()) {
...
You'd need to implement your DiffUtilCallback():
internal class DiffUtilCallback : DiffUtil.ItemCallback<ParentChildren>() {
and its two methods (areContentsTheSame, areItemsTheSame).
And your adapter's two methods:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
viewTypeParent -> YourParentViewHolder(inflater.inflate(R.layout.your_layout_for_parent), parent, false))
viewTypeChildren -> YourChildrenViewHolder(inflater.inflate(R.layout.your_layout_for_children), parent, false))
else -> throw IllegalArgumentException("You must supply a valid type for this adapter")
}
}
I would have an abstract base to simplify the adapter even further:
internal abstract class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(data: ParentChildren)
}
This allows you to have your
// I'm writing pseudo code here... keep it in mind
internal class ParentViewHolder(itemView: View) : BaseViewHolder(itemView) {
private val name: TextView = itemView.findViewById(R.id.item_text)
override fun bind(data: ParentChildren) {
name.text = parentChildren.parent?.name
}
}
internal class ChildrenViewHolder(itemView: View) : BaseViewHolder(itemView) {
private val name: TextView = itemView.findViewById(R.id.item_text)
override fun bind(data: ParentChildren) {
name.text = parentChildren.children?.name
}
}
You get the idea.
Now... ListAdapter<> has a method called submitList(T) where T is the Type of the adapter ParentChildren in the above pseudo-example.
This is as far as I go, and now you have to provide this Activity or Fragment hosting this adapter, the list via either LiveData or whatever is that you prefer for the architecture you have.
It can be a repository passing it to a MutableLiveData inside the viewModel and the ViewModel exposing a LiveData<List<ParentChildren> or similar to the UI.
The sky is the limit.
This shifts the complexity of putting this data together, closer to where the data is, and where the power of SQL/Room can leverage how you combine and process this, regardless of what the UI needs or wants to do with it.
This is my suggestion, but based upon the very limited knowledge I have about your project.
Good luck! :)
Related
I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!
I am trying to perform update & delete operation in a recyclerview with ListAdapter. For this example I am using LiveData to get updates as soon as data is updated.
I don't know why list doesn't shows updated data, but when I see logs it shows correct data.
Code:
#AndroidEntryPoint
class DemoActivity : AppCompatActivity() {
var binding: ActivityDemoBinding? = null
private val demoAdapter = DemoAdapter()
private val demoViewModel: DemoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDemoBinding.inflate(layoutInflater)
setContentView(binding?.root)
initData()
}
private fun initData() {
binding?.apply {
btnUpdate.setOnClickListener {
demoViewModel.updateData(pos = 2, newName = "This is updated data!")
}
btnDelete.setOnClickListener {
demoViewModel.deleteData(0)
}
rvData.apply {
layoutManager = LinearLayoutManager(this#DemoActivity)
adapter = demoAdapter
}
}
demoViewModel.demoLiveData.observe(this, {
it ?: return#observe
demoAdapter.submitList(it)
Log.d("TAG", "initData: $it")
})
}
}
activity_demo.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.DemoActivity">
<Button
android:id="#+id/btn_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:text="Update Data" />
<Button
android:id="#+id/btn_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:text="Delete Data" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#id/btn_update" />
</RelativeLayout>
DemoAdapter:
class DemoAdapter() : ListAdapter<DemoModel, DemoAdapter.DemoViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DemoViewHolder {
val binding =
ListItemDeleteBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DemoViewHolder(binding)
}
override fun onBindViewHolder(holder: DemoViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
inner class DemoViewHolder(private val binding: ListItemDeleteBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(student: DemoModel) {
binding.apply {
txtData.text = student.name + " " + student.visible
if (student.visible) txtData.visible()
else txtData.inVisible()
}
}
}
class DiffCallback : DiffUtil.ItemCallback<DemoModel>() {
override fun areItemsTheSame(oldItem: DemoModel, newItem: DemoModel) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: DemoModel, newItem: DemoModel) =
(oldItem.id == newItem.id) &&
(oldItem.visible == newItem.visible) &&
(oldItem.name == newItem.name)
}
}
DemoViewModel:
class DemoViewModel : ViewModel() {
var demoListData = listOf(
DemoModel(1, "One", true),
DemoModel(2, "Two", true),
DemoModel(3, "Three", true),
DemoModel(4, "Four", true),
DemoModel(5, "Five", true),
DemoModel(6, "Six", true),
DemoModel(7, "Seven", true),
DemoModel(8, "Eight", true)
)
var demoLiveData = MutableLiveData(demoListData)
fun updateData(pos: Int, newName: String) {
val listData = demoLiveData.value?.toMutableList()!!
listData[pos].name = newName
demoLiveData.postValue(listData)
}
fun deleteData(pos: Int) {
val listData = demoLiveData.value?.toMutableList()!!
listData.removeAt(pos)
demoLiveData.postValue(listData)
}
}
Martin's Solution: https://github.com/Gryzor/TheSimplestRV
I suggest you:
Do yourself a favor and add a proper ViewModel/Sealed Class to encapsulate your state.
Initialize your adapter in the usual order:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDeleteBinding.inflate(layoutInflater)
setContentView(binding?.root)
binding.recyclerView.layoutManager = ... (tip: if you won't change the layout manager, I suggest you declare it in the XML directly, skipping this line here. E.g.: app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager")
binding.recyclerView.adapter = yourAdapter
//now observe data which will ultimately lead to `adapter.submitList(...)`
initData()
}
Make sure your DiffUtil.ItemCallback is properly comparing your models. You did old == new in Content, but that's not comparing the content, that's comparing the whole thing. It's the same in this case (I assume, but we haven't seen your Delete model class), but it's best to be explicit about it; the id is not the "content" theoretically speaking for the purposes of this callback thing.
delAdapter.submitList(it.toMutableList()) this is fine, but if you do it (and you do) before the adapter is set, and the LayoutManager is set (as you do), then it's likely possible that the ListAdapter is not magically recomputing it.
Update After Seeing More of Your Code
Let's look at your mutation code (one of the various):
fun updateData(pos: Int, newName: String) {
val listData = demoLiveData.value?.toMutableList()!!
listData[pos].name = newName
demoLiveData.postValue(listData)
}
I see various problems here.
You're grabbing the value from the LiveData. No-Go. LiveData is a value-holder, but I wouldn't "pull it from there" at any time, expect when I receive it via the observation. LiveData is not a repository, it's just holding the value and offering you "guarantees" that it will be managed in conjunction with your lifecycleOwner.
You then use toMutableList() and while this creates a new instance of the List (List<DemoModel> in your case), it does not create a deep copy of the references in the list. Meaning the items in the new (and old) list, are the same, pointing to the exact same spot in memory.
You then perform this operation listData[pos].name = newName in the "new list" but you're effectively modifying the old list as well (you can set a breakpoint there, and inspect the contents of all the lists involved and notice how the same item at pos is now changed to the newName everywhere.
If you want to see even more, put a breakpoint here:
demoViewModel.demoLiveData.observe(this, {
demoAdapter.submitList(it) <--> BREAKPOINT HERE
})
Also put a breakpoint in ListAdapter.java (the android class) in the submitList method:
public void submitList(#Nullable List<T> list) {
mDiffer.submitList(list); ---> BREAKPOINT HERE
}
And when stopped at the 1st breakpoint, observe the value of the list (it) and it's reference. (the first time the breakpoints hit, continue, since we want to observe the list AFTER you mutate the list and not on the "first creation").
Now press your button to change something (update the list) and the breakpoint(s) are going to be hit again, now the submitList call will have a list and it's gonna look like:
notice the Reference: it's (in my example) ArrayList#100073.
Now continue... (the debugger), it will stop again in the mDiffer.submitList(list) line of ListAdapter.
Let's compare.
For the record, this is what I do:
binding.updateButton.setOnClickListener {
viewModel.updateData(0, "Hello World " + 5)
}
So The item at position "0" should be called "Hello World 5" now.
This is already visible here in the debugger:
It's correctly changed in the list, but we're submitting to the adapter... let's see what the adapter has internally (before this is applied), let's jump to the next breakpoint in ListAdapter#submitList():
Notice something strange here?
The item at position 0, is already modified. How?!
Simple, the reference to that object DemoModel is the same. In my example: it's DemoModel#10078.
So how can you prevent this?
Never pass a mutable list to your adapter, always pass a copy (and immutable!)
your Live Data should have been:
var demoLiveData = MutableLiveData(demoList.toList()) //To List creates a new copy of the list, immutable.
This reinforces the concept of a Single Source of Truth. When you mutate data, you need to be sure you know what the scope of the mutation is. The reason why you saw no "change" is because by mutating the data behind the scenes of the adapter, by the time the DiffUtil (Which is async) was called and the change dispatched, the list was already mutated and the Diff Util computed zero changes, which meant the adapter had nothing else to do.
Changing an item in the list, does not (and will never) trigger an adapter to "notify the data was changed", since the adapter is "not observing" the list.
I hope this clarifies your confusion and the importance of not using mutable data all over the place.
Last but not least, I created a super simple project to exercise your problem and pushed it to https://github.com/Gryzor/TheSimplestRV (or if you prefer to see the viewModel alone).
Feel free to look at it (I used one of the default templates so the code is in a Fragment, but... irrelevant of course).
Good luck! :)
Why does NOTIFY DATA SET CHANGED WORK THEN?!
Well, when you do that, you FORCE the adapter to rebind every item, therefore it has to go through the list again (which is changed) and the change is reflected, at the expense of CPU, Battery, flickering, position lost, annoyance to the user(s), etc.
Internally, ListAdapter checks reference of the lists you submit. So you need to create a new list for each update so new one directs another reference different from previous list. Additionally, when you need to update an object in this list, you should create a new object otherwise diff util won't work.
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.
My activity has a Google's ViewModel that fetches some model items. These items are then transformed into adapter items of a RecyclerView. There are also many types of adapter items supported by one RecyclerView.
I would like to have separate view model object for each of these model objects so that I can have more complex logic encapsulated only within that "small" view model.
Currently when I have some asynchronous logic (that needs to be stopped in onCleared()) that is related only to some adapter item I have to somehow route callbacks through main view model so that everything is properly unregistered.
I was considering using ViewModelProvider::get(key, modelClass) but my items are changing over time and I can't find a nice way to "clear" old items.
How are you handling these cases in your projects?
Edit: To add more information about my concern, maybe in different words: I want my "small" ViewModel to live as long as the model item which it represents. It means that:
I must receive onCleared() callback in the same scenarios in which parent of these items receive
I must receive onCleared() callback when item is no longer
Edit: Please try to compare it to a ViewPager with Fragments as items. Every individual model item is represented as a Fragment with its ViewModel. I would like achieve something similar but for RecyclerView.
androidx.lifecycle.ViewModel's are not meant to be used on RecyclerView items by default
Why?
ViewModel is AAC (Android Architecture Component) whose sole purpose is to survive configuration changes of Android Activity/Fragment lifecycle, so that data can be persisted via ViewModel for such case.
This achieved by caching VM instance in storage tied to hosting activity.
That's why it shouldn't be used on RecyclerView (ViewHolder) Items directly as the Item View itself would be part of Activity/Fragment and it (RecyclerView/ViewHolder) doesn't contain any specific API to provide ViewModelStoreOwner (From which ViewModels are basically derived for given Activity/Fragment instance).
Simplistic syntax to get ViewModel is:
ViewModelProvider(this).get(ViewModel::class.java)
& here this would be referred to Activity/Fragment context.
So even if you end up using ViewModel in RecyclerView Items, It would give you same instance due to context might be of Activity/Fragment is the same across the RecyclerView which doesn't make sense to me. So ViewModel is useless for RecyclerView or It doesn't contribute to this case much.
TL;DR
Solution?
You can directly pass in LiveData object that you need to observe from your Activity/Fragment's ViewModel in your RecyclerView.Adapter class. You'll need to provide LifecycleOwner as well for you adapter to start observing that given live data.
So your Adapter class would look something like below:
class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
init {
liveDataToObserve.observe(lifecycleOwner) { t ->
// Notify data set or something...
}
}
}
If this is not the case & you want to have it on ViewHolder class then you can pass your LiveData object during onCreateViewHolder method to your ViewHolder instance along with lifecycleOwner.
Bonus point!
If you're using data-binding on RecyclerView items then you can easily obtain lifecyclerOwner object from your binding class. All you need to do is set it during onCreateViewHolder() something like below:
class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder: ViewHolder {
// Some piece of code for binding
binding.lifecycleOwner = this#RecyclerViewAdapter.lifecycleOwner
// Another piece of code and return viewholder
}
}
class ViewHolder(private val someLiveData: LiveData<T>, binding: ViewDataBinding): RecyclerView.ViewHolder(binding.root) {
init {
someLiveData.observe(requireNotNull(binding.lifecycleOwner)) { t->
// set your UI by live data changes here
}
}
}
So yes, you can use wrapper class for your ViewHolder instances to provide you LiveData out of the box but I would discourage it if wrapper class is extending ViewModel class.
As soon as concern about mimicking onCleared() method of ViewModel, you can make a method on your wrapper class that gets called when ViewHolder gets recycled or detaches from window via method onViewRecycled() or onViewDetachedFromWindow() whatever fits best in your case.
Edit for comment of #Mariusz: Concern about using Activity/Fragment as LifecycleOwner is correct. But there would be slightly misunderstanding reading this as POC.
As soon as one is using lifecycleOwner to observe LiveData in given RecyclerViewHolder item, it is okay to do so because LiveData is lifecycle aware component and it handles subscription to lifecycle internally thus safe to use. Even if you can explicitly remove observation if wanted to, using onViewRecycled() or onViewDetachedFromWindow() method.
About async operation inside ViewHolder:
If you're using coroutines then you can use lifecycleScope from lifecycleOwner to call your operation and then provide data back to particular observing LiveData without explicitly handling clear out case (LifecycleScope would take care of it for you).
If not using Coroutines then you can still make your asyc call and provide data back to observing LiveData & not to worry about clearing your async operation during onViewRecycled() or onViewDetachedFromWindow() callbacks. Important thing here is LiveData which respects lifecycle of given LifecycleOwner, not the ongoing async operation.
Don't know if google has nice support for nested ViewModel's, looks like not.
Thankfully, we don't need to stick to androidx.lifecycle.ViewModel to apply MVVM approach where we need. And there is a small example I decided to write:
Fragment, nothing changes:
#Override public void onCreate(#Nullable Bundle savedInstanceState) {
final ItemListAdapter adapter = new ItemListAdapter();
binding.getRoot().setAdapter(adapter);
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
viewModel.getItems().observe(getViewLifecycleOwner(), adapter::submitList);
}
ItemListAdapter, in addition to populate view, it also becomes responsible for notifying item's observers - should they continue to listen, or not. In my example adapter was ListAdapter which extends RecyclerView.Adapter, so it receives list of items. This is unintentionally, just edited some code I already have. It's probably much better to use different base implementation, but it's acceptable for demonstration purposes:
#Override public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
return new Holder(parent);
}
#Override public void onBindViewHolder(Holder holder, int position) {
holder.lifecycle.setCurrentState(Lifecycle.State.RESUMED);
holder.bind(getItem(position));
}
#Override public void onViewRecycled(Holder holder) {
holder.lifecycle.setCurrentState(Lifecycle.State.DESTROYED);
}
// Idk, but these both may be used to pause/resume, while bind/recycle for start/stop.
#Override public void onViewAttachedToWindow(Holder holder) { }
#Override public void onViewDetachedFromWindow(Holder holder) { }
Holder. It implements LifecycleOwner, which allows to unsubscribe automatically, just copied from androidx.activity.ComponentActivity sources so all should be okay :D :
static class Holder extends RecyclerView.Holder implements LifecycleOwner {
/*pkg*/ LifecycleRegistry lifecycle = new LifecycleRegistry(this);
/*pkg*/ Holder(ViewGroup parent) { /* creating holder using parent's context */ }
/*pkg*/ void bind(ItemViewModel viewModel) {
viewModel.getItem().observe(this, binding.text1::setText);
}
#Override public Lifecycle getLifecycle() { return lifecycle; }
}
List view-model, "classique" androidx-ish ViewModel, but very rough, also provide nested view models. Please, pay attention, in this sample all view-models start to operate immediately, in constructor, until parent view-model is commanded to clear! Don't Try This at Home!
public class ItemListViewModel extends ViewModel {
private final MutableLiveData<List<ItemViewModel>> items = new MutableLiveData<>();
public ItemListViewModel() {
final List<String> list = Items.getInstance().getItems();
// create "nested" view-models which start background job immediately
final List<ItemViewModel> itemsViewModels = list.stream()
.map(ItemViewModel::new)
.collect(Collectors.toList());
items.setValue(itemsViewModels);
}
public LiveData<List<ItemViewModel>> getItems() { return items; }
#Override protected void onCleared() {
// need to clean nested view-models, otherwise...
items.getValue().stream().forEach(ItemViewModel::cancel);
}
}
Item's view-model, using a bit of rxJava to simulate some background work and updates. Intentionally I do not implement it as androidx....ViewModel, just to highlight that view-model is not what google names ViewModel but what behaves as view-model. In actual program it most likely will extend, though:
// Wow, we can implement ViewModel without androidx.lifecycle.ViewModel, that's cool!
public class ItemViewModel {
private final MutableLiveData<String> item = new MutableLiveData<>();
private final AtomicReference<Disposable> work = new AtomicReference<>();
public ItemViewModel(String topicInitial) {
item.setValue(topicInitial);
// start updating ViewModel right now :D
DisposableHelper.set(work, Observable
.interval((long) (Math.random() * 5 + 1), TimeUnit.SECONDS)
.map(i -> topicInitial + " " + (int) (Math.random() * 100) )
.subscribe(item::postValue));
}
public LiveData<String> getItem() { return item; }
public void cancel() {
DisposableHelper.dispose(work);
}
}
Few notes, in this sample:
"Parent" ViewModel lives in activity scope, so all its data (nested view models) as well.
In this example all nested vm start to operate immediately. Which is not what we want. We want to modify constructors, onBind, onRecycle and related methods accordingly.
Please, test it on memory leaks.
Although that is true that Android uses ViewModels in Android Architecture Components it does not mean that they are just part of AAC. In fact, ViewModels are one of the components of the MVVM Architecture Pattern, which is not Android only related. So ViewModel's actual purpose is not to preserve data across Android's lifecycle changes. However, because of exposing its data without having a View's reference makes it ideal for the Android specific case in which the View can be recreated without affecting to the component that holds its state (the ViewModel). Nonetheless, it has other benefits such as facilitating the Separation of Concerns among others.
It is also important to mention that your case can not be 100% compared to the ViewPager-Fragments case, as the main difference is that the ViewHolders will be recycled between items. Even if ViewPager's Fragments are destroyed and recreated, they will still represent the same Fragment with that same data. That is why they can safely bind the data provided by their already existing ViewModel. However, in the ViewHolder case, when it is recreated, it can be representing a totally new item, so the data its supposed ViewModel could be providing may be incorrect, referencing the old item.
That being said you could easily make the ViewHolder become a ViewModelStoreOwner:
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewModelStoreOwner {
private var viewModelStore: ViewModelStore = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = viewModelStore
}
This can still be useful if the data provided by the ViewModel is the same independently of the ViewHolder's item (shared state between all items). However, if that is not the case, then you would need to invalidate the ViewModelStore by calling viewModelStore.clear() and create a new ViewModel instance probably in ViewHolder's onViewRecycled. You will loose the advantage of keeping the state no matter the view's lifecycle, but can sometimes still be useful as to follow Separation of Concerns.
Finally, regarding to the option of using a LiveData instance to control the state, no matter if it is provided by a ViewHolder's shared or specific ViewModel or it is passed through the Adapter, you will need a LifecycleOwner to observe it. A better approach to using the current Fragment or Activity lifecycle is to just use the specific ViewHolder's actual lifecycle, as they are actually created and destroyed, by making them implement the LifecycleOwner interface. I created a small library which does exactly that.
I followed this wonderfull answer HERE by aeracode with a one exception. Instead of ViewModel I've used Rx BehaviourSubject that work perfectly for me.
In case of coroutines You can use alternatively StateFlow.
clas MyFragment: Fragment(){
private val listSubject = BehaviorSubject.create<List<Items>>()
...
private fun observeData() {
viewModel.listLiveData.observe(viewLifecycleOwner) { list ->
listSubject.onNext(list)
}
}
}
RecyclerView
class MyAdapter(
private val listObservable: BehaviorSubject<List<Items>>
) : RecyclerView.Adapter<MyViewHolder>() {
[...]
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bindToData(getItem(position))
}
override fun onViewRecycled(holder: MyViewHolder) {
holder.onViewRecycled()
}
...
class MyViewHolder(val binding: LayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
private var disposable: Disposable? = null
fun bindToData(item: Item) = with(binding) {
titleTv.text = item.title
disposable = listObservable.subscribe(::setItemList) <- Here You listen
}
fun onViewRecycled() {
disposable?.dispose()
}
}
I have an app structured in MVVM. I have different fragments within the same activity. Each fragment has its own ViewModel and all data are retrieved from a REST API.
In FragmentA, there is a RecyclerView that lists X class instances. I want to set OnClickListener on the RecyclerView and I want to pass related X object to FragmentB when an item clicked in the RecyclerView. How can I achieve this?
How I imagine it is the following.
The Fragment passes a listener object to the adapter, which in turn passes it to the ViewHolders
Here is a quick sketch of how it should look like
class Fragment {
val listener = object: CustomAdapter.CustomViewHolderListener() {
override fun onCustomItemClicked(x: Object) {}
}
fun onViewCreated() {
val adapter = CustomAdapter(listener)
}
}
---------------
class CustomAdapter(private val listener: CustomViewHolderListener) {
val listOfXObject = emptyList() // this is where you save your x objects
interface CustomViewHolderListener{
fun onCustomItemClicked(x : Object)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.itemView.setOnClickListener {
listener.onCustomItemClicked(listOfXObject[position])
}
}
}
Here are some articles that might help you get the general gist of the things.
They don't answer your question directly though
Hope it is helpful
link 1 link 2
if you're using data binding you need to pass your view(which is Fragment in your case) into the layout via adapter class and you need to import your view in layout file to be able to call view's method
android:onClick="#{() -> view.onXXXClick(item)}"
pass your current model class which is item into this new method and then create onXXXClick method in your view and do whatever you wish.
if you will be doing view related operations such as navigation from one fragment to another or starting a service you should create above function in your view, if you're doing network or db related operations it should be in your ViewModel
you can check out my GitHub repository to understand better.