Leanback create different custom Row Views - android

I am using the Leanback library and I would like to know how to create multiple custom Row Views. For creating different items in a row you need to extend PresenterSelector
I tried doing the same for the ListRowPresenter but couldn't achieve the right result.
No row was binded in the RowsSupportFragment and in the logs the getPresenter method from PresenterSelector was called multiple times until out of memory.

For solving this I had to check the leanback showcase repository
Based on the class ShadowRowPresenterSelector I managed to find how to create a selector for my custom RowPresenters.
class ShadowRowPresenterSelector : PresenterSelector() {
private val aCustomListRowPresenter by lazy { ACustomListRowPresenter() }
private val bCustomListRowPresenter by lazy { BCustomListRowPresenter() }
override fun getPresenter(item: Any): Presenter {
return when (item) {
is ARowVM -> {
aCustomListRowPresenter
}
is BRowVM -> {
bCustomListRowPresenter
}
else -> aCustomListRowPresenter
}
}
override fun getPresenters(): Array<Presenter> {
return arrayOf(aCustomListRowPresenter, bCustomListRowPresenter)
}
}
What caused the method getPresenter to be called multiple times for me was that there I by mistake created every time a new object for my custom row presenter.
I hope this will help someone in the future.

Related

In Android Kotlin, what's the right way to pass a onclick event into a viewholder?

Is there any difference in these two ways?
I've been using the seond way and it works so far, yet I found the first way upon reading tutorial articles.
1st:
class FlowersAdapter(private val onClick: (Flower) -> Unit) :
ListAdapter<Flower, FlowersAdapter.FlowerViewHolder>(FlowerDiffCallback) {
/* ViewHolder for Flower, takes in the inflated view and the onClick behavior. */
class FlowerViewHolder(itemView: View, val onClick: (Flower) -> Unit) :
RecyclerView.ViewHolder(itemView) {
private val flowerTextView: TextView = itemView.findViewById(R.id.flower_text)
private val flowerImageView: ImageView = itemView.findViewById(R.id.flower_image)
private var currentFlower: Flower? = null
init {
itemView.setOnClickListener {
currentFlower?.let {
onClick(it)
}
}
}
/* Bind flower name and image. */
fun bind(flower: Flower) {
currentFlower = flower
flowerTextView.text = flower.name
if (flower.image != null) {
flowerImageView.setImageResource(flower.image)
} else {
flowerImageView.setImageResource(R.drawable.rose)
}
}
}
}
First way of writing
2nd:
class FlowerViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private val flowerTextView: TextView = itemView.findViewById(R.id.flower_text)
private val flowerImageView: ImageView = itemView.findViewById(R.id.flower_image)
private var currentFlower: Flower? = null
/* Bind flower name and image. */
fun bind(flower: Flower) {
currentFlower = flower
flowerTextView.text = flower.name
if (flower.image != null) {
flowerImageView.setImageResource(flower.image)
} else {
flowerImageView.setImageResource(R.drawable.rose)
}
itemView.setOnClickListener {
onClick(flower)
}
}
}
Second way of writing
Appreicate your time and effort in telling me the differences.
From the perceptive of separation of concern, all the clickListeners are supposed to be handled in the Activity or Fragment and Adapters are meant just to wrap around the items, in your case Flower and present them in a way which can be used by the RecyclerView to display on the screen.
With that being said, the core logic of clickListeners is to be moved out of the bind method into the activity/fragment and that's precisely whats the firstMethod is all about. Matter of fact, I haven't noticed any performance improvement by employing the FirstMethod over the second one yet I insist on using FirstOne because its more of code organizing.
IMHO I feel like the adapter should know nothing about click listeners or any details about the ViewHolder; so I wouldn't pass the callback through the adapter.
I like passing the callback to my ViewHolder but instead of mapping into the init block I do it on the onBind hook from the adapter where I receive the view as a parameter. Also, I pass or update the ViewHolders directly into my Adapters. And then have some generic functions to compute whether the data-set has changed or not.
If you do it like this, you have the benefit that you may build 1 generic adapter and use it elsewhere without really minding how many different types of ViewHolder you may have to implement later on as it is completely agnostic.
TLDR;
So based on what you've provided us I would use the good things of both approaches. Binding the callback into the bind hook and passing the callback through the constructor of the ViewHolder

Add additional data to RecyclerView items

I have a functioning RecyclerView which works fine with the given data I provide via ListAdapter as shown below. What I now want is to add additional data to my list items.
class IngredientAdapter(
private val ingredientClickListener: IngredientClickListener
) : ListAdapter<Ingredient, RecyclerView.ViewHolder>(EventDiffCallback()) {
private val adapterScope = CoroutineScope(Dispatchers.Default)
fun submitIngredientsList(list: List<Ingredient>?) {
adapterScope.launch {
withContext(Dispatchers.Main) {
submitList(list)
}
}
}
I have no idea how to do that properly or if RecyclerViews are even capable of doing this. The only way I am able to see is merging both data classes (Ingredient plus the new one) together and submit them as list together to the adapter but this seems messy and I am looking for a better way.
So my question is: How to feed data into my list items without merging it together with the data I already have? Is RecyclerView the wrong choice in my case?
Thanks in advance!
Ok I found a solution: I submitted the additional data list just how like the other one but did not attach it directly to the ListAdapter since this is not possible.
In the function onBindViewHolder after getting the item for the current position I use this information to retrieve the correct element from the new data list. Then I attach the data to the view by calling using the viewholders view binding
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val resources = holder.itemView.context.resources
val ingredientItem = getItem(position)
holder.bind(ingredientClickListener, ingredientItem)
val groceryInStock: GroceryInStock? = availableGroceries.firstOrNull{
ingredientItem.grocery.groceryId == it.grocery.groceryId
}
holder.binding.listItemAvailableAmount.text = groceryInStock.amount
}
}
}
Since the data I add fully depends on the already existing item being displayed I did not make any changes to the functions areItemsTheSame and areContentsTheSame in my overriden DiffUtil.ItemCallback class.

How to set OnClickListener on RecyclerView item in MVVM structure

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.

Nested Recyclerviews with Complex Room LiveData

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! :)

NotifyDataSetChanged does not update the RecyclerView correctly

I am trying to implement a fairly basic logic within my recyclerview adapter but notifyDataSetChanged() is giving me quite the headache.
I have a filter method that looks like this:
fun filter(category: Int) {
Thread(Runnable {
activeFiltered!!.clear()
if (category == -1) {
filterAll()
} else {
filterCategory(category)
}
(mContext as Activity).runOnUiThread {
notifyDataSetChanged()
}
}).start()
}
where filterAll() and filterCategory() functions are quite easy:
private fun filterAll() {
activeFiltered?.addAll(tempList!!)
}
private fun filterCategory(category: Int) {
for (sub in tempList!!) {
if (sub.category == category) {
activeFiltered?.add(sub)
}
}
}
When I run this code and filter the list by category the activeFiltered list is updated correctly and contains the items I expect, but when notifyDataSetChanged() is run it only cuts the list's range without updating the items.
Is there a way to fix this?
I also tried, instead of notifyDataSetChanged() to use:
activeFiltered!!.forEachIndexed {index, _ -> notifyItemChanged(index)}
but the problem is still there.
It isn't a threading issue either since I tried putting the whole logic in the main thread and the list still wasn't updated correctly.
This is my onBindViewHolder():
override fun onBindViewHolder(viewHolder: ActiveViewHolder, pos: Int) {
sub = activeFiltered!![pos]
inflateView()
}
This is where I inflate my text, sub is the instance variable set in the onBindViewHolder():
private fun inflateView() {
viewHolder.title.text = sub.title
}
It seems the implementation of onBindViewHolder() is incorrect. In order to update a list item, the passed in viewHolder parameter should be used (not the viewHolder you created in the onCreateViewHolder()).
The correct implementation should be like
override fun onBindViewHolder(viewHolder: ActiveViewHolder, pos: Int) {
val sub = activeFiltered!![pos]
inflateView(viewHolder, sub)
}
private fun inflateView(viewHolder: ActiveViewHolder, sub: <YourDataType>) {
viewHolder.title.text = sub.title
}
By the way, it is not a good practice to hold something as a member field in order to access it in several methods. Feel free to pass it as arguments to such methods. In the above code I passed the sub as argument and not stored it as a member.
And also it is not necessary to hold the viewHolder that you create in onCreateViewHolder(). We mostly need them in some callback methods (like onBindViewHolder(), etc) and these methods will receive the right viewHolder as arguments.
I think you are using the original array in onBindView() instead of the filtered one.

Categories

Resources