Add additional data to RecyclerView items - android

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.

Related

How should I go about implementing MVVM architecture pattern in my project?

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!

Calling notifyDataSetChanged and updating the data, but RecyclerViewer still takes in old data

I'm currently experimenting with a RecyclerViewer, but stumbled upon a problem: When I update the data and call notifyDataSetChanged, the RecyclerViewer updates it's view, but not with the new data, but rather with the old data.
I've searched through Stackoverflow for the problem, but in most cases the problem is that they either created two instances of the adapter (reference) or that they don't have a layout manager, and I believe that neither of those is my problem.
Here is my code for creating and updating the RecyclerViewer in the fragment in which it's hosted:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_player_list_list, container, false)
data = getPlayersAsList(requireContext(), gameUUID)
if(data.isEmpty()){
data = listOf(SharedPreferencesManager.Companion.Player("John", false))
}
// Set the adapter
if(view is LinearLayout){
view.children.forEach {
if (it is RecyclerView) {
with(it) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
Log.i("DEBUG", "The first adapter was called")
adapter = MyItemRecyclerViewAdapter(data)
}
}
}
}
fun notifyDataUpdate(position: Int? = null) {
if(view is LinearLayout){
(view as LinearLayout).children.forEach {
if(it is RecyclerView){
data = getPlayersAsList(requireContext(), gameUUID)
Log.i("DATA UPDATE", "Player list is now $data")
it.adapter?.notifyDataSetChanged()
}
}
}
}
And here is the code of the adapter:
import android.util.Log
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.chuaat.hideandseek.databinding.FragmentPlayerListBinding
import com.google.android.material.button.MaterialButton
import java.util.*
/**
* [RecyclerView.Adapter] that can display a [PlaceholderItem].
* TODO: Replace the implementation with code for your data type.
*/
class MyItemRecyclerViewAdapter(
private val values: List<SharedPreferencesManager.Companion.Player>
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentPlayerListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
Log.i("RECYCLER VIEWER", "SETTING VALUE OF $item")
if(item.isSeeker){
holder.buttonView.setIconResource(R.drawable.ic_baseline_search_24)
}
else{
holder.buttonView.setIconResource(R.drawable.ic_outline_visibility_off_24)
}
holder.contentView.text = item.name
}
override fun getItemCount(): Int = values.size
inner class ViewHolder(binding: FragmentPlayerListBinding) :
RecyclerView.ViewHolder(binding.root) {
val buttonView: MaterialButton = binding.toggleSeekerButton as MaterialButton
val contentView: TextView = binding.content
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
}
The log output I get when calling notifiyDataUpdate is:
I/DATA UPDATE: Player list is now [Player(name=Joe, isSeeker=false)]
I/RECYCLER VIEWER: SETTING VALUE OF Player(name=John, isSeeker=false)
As you can see the updated data is with a Player named Joe, but in onBindViewHolder the only value is the default Player ("John").
What is the problem I'm missing?
You're not actually updating the data in your adapter
Here's how you initialise it:
data = getPlayersAsList(requireContext(), gameUUID)
if(data.isEmpty()){
data = listOf(SharedPreferencesManager.Companion.Player("John", false))
}
...
adapter = MyItemRecyclerViewAdapter(data)
and that parameter in your adapter's constructor is your data source for the RecyclerView
class MyItemRecyclerViewAdapter(
private val values: List<SharedPreferencesManager.Companion.Player>
At this point, your Fragment has a list called data which contains your current data, and the Adapter has a reference to that same list. They're both looking at the same object - let's call it list 1.
Then you update your data in the Fragment, and notify the Adapter:
data = getPlayersAsList(requireContext(), gameUUID)
it.adapter?.notifyDataSetChanged()
But what you've done is create a new list, list 2, with that getPlayersAsList call. You assign that to data. So data points to list 2, the new data - but values in your adapter still points to list 1, the old list. So for the adapter, nothing's changed! It can't see the new data, so even though you notify it, it will still look the same.
You have two options here. Firstly, since you're already using this shared list that the Fragment and Adapter are both looking at, you can just update that list - which is what you should be doing if they're both sharing it, right?
// clear the old data
data.clear()
// replace it with the new items
// this is the same as data.addAll(getPlayersAsList(...))
data += getPlayersAsList(requireContext(), gameUUID)
// now the list the adapter is using has been updated, you can notify it
it.adapter?.notifyDataSetChanged()
This way you're updating the actual list the adapter uses as its dataset, so you'll see the changes when it refreshes.
The second way, and the one I'd recommend, is to completely separate the Adapter's data from anything the Fragment is holding onto. Having a shared list like this can be a source of bugs, where one component changes it in the background, affecting the state of another component. If you ever use a DiffUtil in a RecyclerView for example, mutating the current list will stop it from working, because it won't be able to compare for changes.
You could make the values property a public var and update that externally, then notify the adapter - but honestly, it's better to let the Adapter handle those details internally. A setter function is a lot cleaner to me:
// in the Adapter
private var values = emptyList<SharedPreferencesManager.Companion.Player>()
fun setData(items: List<SharedPreferencesManager.Companion.Player>) {
// as a safety measure, creating a new list like this *ensures* that if the one
// that was passed in is mutated, this internal one won't change. (The items
// in the list can still be mutated of course!)
values = items.toList()
// now the -adapter- can decide on how/if it should update, based on its own
// internal state and the new data. The Fragment shouldn't be concerned with those details
notifyDataSetChanged()
}
Then when you want to update the adapter, just pass it the new data list:
// assuming you still want to keep a local reference to this data (if you don't need it, don't!)
data = getPlayersAsList(requireContext(), gameUUID)
// I'd really recommend just keeping a reference to your adapter when you create it,
// so you don't need to go searching for it and casting it like this
(it.adapter as? MyItemRecyclerViewAdapter)?.setData(data)
To initialise the adapter, you can either use this method:
// seriously, just store this in a `lateinit var adapter: MyItemRecyclerViewAdapter`
adapter = MyItemRecyclerViewAdapter()
adapter.setData(data)
recyclerView.adapter = adapter
Or you could keep the constructor parameter (which we're not using as a property to store the data, remember!) and use it to call the setData function:
class MyItemRecyclerViewAdapter(
data: List<SharedPreferencesManager.Companion.Player> // no val
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
init {
setData(data)
}
And just as a hint - the way you're accessing your RecyclerView is complicated and not how you generally do things in Android. Give it an id in your layout XML file (R.layout.fragment_player_list_list) and then just do
val recyclerView = view.findViewById<RecyclerView>(R.id.whatever)
That's it! No need to loop through the hierarchy searching for it. If you store your Adapter in a variable, you probably won't need to touch the RV itself after setting it up - just call setData on your adapter reference

ListAdapter Diff does not dispatch updates on same list instance, but neither on different list from LiveData

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.

Android - OnResume inside onBindViewHolder in Recyclerview

I've got an RecyclerView that lists articles, in each article/item in the onBindViewHolder I'm loading another RecyclerView for the comments and I need to update them when the user returns to the activity.
Is there a way to detect the activity OnResume inside onBindViewHolder of the parent RecyclerView?
I assume that you have some data class for article which holds its details and comments. You pass this list to first recycler which in onBind is filling comment recyclers with data. If you have to update comments just pass list of articles with updated comments to article's adapter. It will trigger onBindViewHolder inside which you will have new comments. In code it would be somethings like this:
data class Article (
val name: String
... other details
val comments: List<Comment>
)
onBindViewHolder for Article's adapter:
override fun onBindViewHolder(holder: Holder, position: Int) {
val article = getItem(position)
// here you should submit list of comments
commentsAdapter.submitList(article.comments)
}
and in Activity:
override fun onResume() {
super.onResume()
//get new comments here and set it for new articles
val newArticles = oldArticles.map { article -> it.copy(comments = newComments)}
articlesAdapter.submitList(newArticles)
}
By the way it seems like AdapterDelegates would be even better solution to your problem. Read more here: http://hannesdorfmann.com/android/adapter-delegates

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