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:
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! :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With