I have an App that is communicating with a meshNetwork and constantly receiving messages over wifi with the properties of meshNodes. Those nodes should be displayed in a RecyclerView and also updated when a property changes with the usage of LiveData.
However when I receive multiple messages at almost the same time, the RecyclerView does not update the list.
E.g. a message form a meshNode is received, if the node is not already in a list inside LiveData<List<>> it will be added. After adding it to the list, the bound recycler view displays the node, everything is perfect by now. Immediatly after a new message from another meshNode that is not in the list is received, and added to the list, the meshNode is not displayed in the RecyclerView.
I really dont know why, and every help will be appreciated.
MeshNodeHandler handles messages received from meshNetwork, and updating nodes list
class MeshNodeHandler() : MeshHandler.MeshListener() {
private val mMeshNodes= mutableListOf<MeshNode>()
private val mMeshNodesLiveData = MutableLiveData<List<MeshNode>> = MutableLiveData()
val meshNodes: LiveData<List<MeshNode>>= mMeshNodesLiveData
override fun onNodeMessageReceived(nodeMessage: NodeMessage) {
val node =
mMeshNodes.firstOrNull {
it.meshID == nodeMessage.meshID
}
if (node != null && checkIfNodePropertiesChanged(node, nodeMessage)) {
Timber.d("Update mesh node")
// Update node in List...
mMeshNodesLiveData.postValue(mMeshNodes)
} else if (node == null) {
Timber.d("Add mesh node")
// Add node to list
mMeshNodesLiveData.postValue(mMeshNodes)
}
}
}
MeshNodesListViewModel just exposing the list from the NodeHandler
class MeshNodesListViewModel #Inject constructor(
private val meshNodeHandler: MeshNodeHandler
) : ViewModel() {
val meshNodes: LiveData<List<MeshNode>> = meshNodeHandler.meshNodes
}
And MeshNodesListFragment that observes the LiveData and submits the list to the adapter
class MeshNodesListFragment : BaseFragment<FragmentMeshNodesListBinding, MeshNodesListViewModel>(
layoutId = R.layout.fragment_mesh_nodes_list
) {
#Inject
lateinit var viewAdapter: MeshNodesAdapter
override fun onInitDataBinding() {
// DataBinding stuff ...
viewBinding.meshNodesRecyclerView.apply {
adapter = viewAdaper
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.meshNodes.observe(viewLifecycleOwner, {
Timber.d("Submit list")
viewAdapter.submitList(it)
})
}
}
Logcat:
D/MeshNodeHandler: Add mesh node
D/MeshNodesListFragment: Submit List
...
D/MeshNodehandler: Add mesh node
D/MeshNodesListFragment: Submit List
So the log says that a second node has been added to the LiveData<List<>>, and the list should also be submitted to the Adaper, but there is no second item displayed in the RecyclerView. If I destroy the Fragment and creating it again by switchig to portrait mode, then both items are displayed.
I'm thankful for every reply, cheers and stay healthy!
I think you are missing the notifyDataSetChanged() after you update the list. My understanding is that the list is updated but not reflecting in the view because recyclerView is not updating it's views. Try to call notifyItemInserted() or notifyItemRangeInserted() variants based on if you are adding single or multiple items. Hope this resolves the issue. Let me know if it helps.
Related
In my case I am trying to get a list of String from my Adapter and use it in my Fragment but upon debugging using Logs I found that the list is getting updated inside the onBindViewHolder but not outside it. So when I try to access the list from my Fragment I am getting an empty list of String.
I have spent few hours trying to figure this but can't find a feasible solution.
My Approach: I am thinking of an approach to save this list in a room table and then query it back in the Fragment. Though it may solve the issue but is it the only way? Are there any other ways to achieve this result?
My Adapter
class FloorProfileDialogAdapter() : RecyclerView.Adapter<FloorProfileDialogAdapter.MyViewHolder>() {
var floors = emptyList<String>()
inner class MyViewHolder(val binding: ScheduleFloorDialogItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ScheduleFloorDialogItemBinding.inflate(inflater, parent, false)
return MyViewHolder(binding)
}
private val checkedFloors: MutableList<String> = mutableListOf()
//List of uniquely selected checkbox to be observed from New Schedule Floor Fragment
var unique: List<String> = mutableListOf()
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentFloor = floors[position]
Timber.d("Current floor: $currentFloor")
holder.binding.floorCheckBox.text = "Floor $currentFloor"
//Checks the checked boxes and updates the list
holder.binding.floorCheckBox.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isChecked) {
Timber.d("${buttonView.text} checked")
checkedFloors.add(buttonView.text.toString())
} else if (!buttonView.isChecked) {
Timber.d("${buttonView.text} unchecked")
checkedFloors.remove(buttonView.text)
}
unique = checkedFloors.distinct().sorted()
Timber.d("List: $unique")
}
}
fun returnList(): List<String> {
Timber.d("$unique")
return unique
}
override fun getItemCount(): Int {
return floors.size
}
#SuppressLint("NotifyDataSetChanged")
fun getAllFloors(floorsReceived: List<String>) {
Timber.d("Floors received : $floorsReceived")
this.floors = floorsReceived
notifyDataSetChanged()
}
}
Fragment code where I am trying to read it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Chosen Floors
val chosenFloors = floorProfileDialogAdapter.returnList()
Timber.d("Chosen floors : $chosenFloors")
}
Note: The list I am trying to receive is var unique: List<String> = mutableListOf. I tried to get it using the returnList() but the log in that function shows that list is empty. Similarly the Log in fragment shows that it received an empty list.
Edit 1 :
Class to fill the Adapter Floors using getAllFloors()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val floorList: MutableList<String> = mutableListOf()
var profileName: String? = ""
profileName = args.profileName
//Profile name received
Timber.d("Profile name : $profileName")
//Getting list of all floors
createProfileViewModel.totalFloors.observe(viewLifecycleOwner) {
Timber.d("List of floors received : $it")
val intList = it.map(String::toInt)
val maxFloorValue = intList.last()
var count = 0
try {
while (count <= maxFloorValue) {
floorList.add(count.toString())
count++
}
} catch (e: Exception) {
Timber.d("Exception: $e")
}
floorProfileDialogAdapter.getAllFloors(floorList)
Timber.d("Floor List : $floorList")
}
When you first set up your Fragment and create your Adapter, unique is empty:
var unique: List<String> = mutableListOf()
(if you have some checked state you want to save and restore, you'll have to initialise this with your checked data)
In onViewCreated, during Fragment setup, you get a reference to this (empty) list:
// Fragment onViewCreated
val chosenFloors = floorProfileDialogAdapter.returnList()
// Adapter
fun returnList(): List<String> {
return unique
}
So chosenFloors is a reference to this initial entry list. But when you actually update unique in onBindViewHolder
unique = checkedFloors.distinct().sorted()
you're replacing the current list with a new list object. You're not updating the existing list (even though you made it a MutableList). So you never actually add anything to that empty list you started with, and chosenFloors is left pointing at a list that contains nothing, while the Adapter has discarded it and unique holds a completely different object.
The solution there is to make unique a val (so you can't replace it) and just change its contents, e.g.
unique.clear()
unique += checkedFloors.distinct().sorted()
But I don't feel like that's your problem. Like I pointed out, that list is initially empty anyway, and you're grabbing it in your Fragment during initialisation just so you can print out its contents, as though you expect it to contain something at that point. Unless you initialise it with some values, it's gonna be empty.
If you're not already storing/restoring them, you'll need to handle that! I posted some code to do that on another answer so I'll just link that instead of repeating myself. That code is storing indices though, not text labels like you're doing. Indices are much cleaner and avoid errors - the text is more of a display thing, a property of the item the specific (and unique) index refers to. (But you can store a string array in SharedPreferences if you really want to.)
Also you're not actually updating your ViewHolder to display the checked state for the current item in onBindViewHolder. So whatever ViewHolder you happen to have been given (there's only a few of them for the list, they get reused) it's just showing whatever its checkbox was last set to, by you poking at it. Check an item, then scroll the list and see what happens!
So you need to check or uncheck the box so it's correct for the item you're displaying. This is pretty easy if you're storing the checked items by indices:
// explicitly set the checked state, depending on whether the item at 'position' is checked
holder.binding.floorCheckBox.checked = checkedItems[position]
You can work out something similar for your text label approach, but again I wouldn't recommend doing things that way.
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
it is a known issue that ListAdapter (actually the AsyncListDiffer from its implementation) does not update the list if the new list only has modified items but has the same instance. The updates do not work on new instance list either if you use the same objects inside.
For all of this to work, you have to create a hard copy of the entire list and objects inside.
Easiest way to achieve this:
items.toMutableList().map { it.copy() }
But I am facing a rather weird issue. I have a parse function in my ViewModel that finally posts the items.toMutableList().map { it.copy() } to the LiveData and gets observes in the fragment. Even with the hard copy, DiffUtil does not work. If I move the hard copy inside the fragment, then it works.
To get this easier, if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items)
... then, it doesn't work. But if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items.toMutableList().map { it.copy() })
... then it works.
Can anybody explain why this doesn't work?
In the mean time, I have opened an issue on the Google Issue Tracker because maybe they will fix the AsyncListDiffer not updating same instance lists or items. It defeats the purpose of the new adapter. The AsyncListDiffer SHOULD ALWAYS accept same instance lists or items, and fully update using the diff logic that the user customises in the adapter.
I made a quick sample using DiffUtil.Callback and ListAdapter<T, K> (so I called submitList(...) on the adapter), and had no issues.
Then I modified the adapter to be a normal RecyclerView.Adapter and constructed an AsyncDiffUtil inside of it (using the same DiffUtil.Callback from above).
The architecture is:
Activity -> Fragment (contains RecyclerView).
Adapter
ViewModel
"Fake Repository" that simply holds a val source: MutableList<Thing> = mutableListOf()
Model
I've created a Thing object: data class Thing(val name: String = "", val age: Int = 0).
For readability I added typealias Things = List<Thing> (less typing). ;)
Repository
It's fake in the sense that items are created like:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
But the "source" is a mutableList of (the typealias).
The other thing the repo can do is "simulate" a modification on a random item. I simply create a new data class instance, since it's obviously all immutable data types (as they should be). Remember this is just simulating a real change that may have come from an API or DB.
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
ViewModel
Nothing fancy here, it talks and holds the reference to the ThingsRepository, and exposes a LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
And the "state" is:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
The viewModel has two public methods (Aside from the val state):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
Nothing special, just a way to modify a random item by position (remember this is just a quick hack to test it).
So FetchData, launches the async code in IO to "fetch" (in reality, if the list is there, the cached list is returned, only the 1st time the data is "made" in the repo).
Modify data is simpler, calls modify on the repo and fetch data to post the new value.
Adapter
Lots of boilerplate... but as discussed, it's just an Adapter:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback is just:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
This Adapter now has an AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this in this context is the actual adapter (needed by the differ) and DiffUtilCallback is just a DiffUtil.Callback implementation:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
nothing special here.
The only special methods in the adapter (aside from onCreateViewHolder and onBindViewHolder) are these:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
So we ask the differ to do these for us and expose the public method submitList to emulate a listAdapter#submitList(...), except we delegate to the differ.
Because you may be wondering, here's the ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
Don't be too harsh, I know i passed the click listener directly, I only had about 1 hour to do all this, but nothing special, the layout it's just two text views (age and name) and we set the whole row clickable to pass the position to the callback. Nothing special here either.
Last but not least, the Fragment.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
It has 3 member variables. The ViewModel, the Binding (I used ViewBinding why not it's just 1 liner in gradle), and the Adapter (which takes the Click listener in the ctor for convenience).
In this impl., I simply call the viewmodel with "modify item at position (X)" where X = the position of the item clicked in the adapter. (I know this could be better abstracted but this is irrelevant here).
there's only two other implemented methods in this fragment...
onDestroy:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
Anyway, the other is unsurprisingly, onCreateView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
Bind the thing (root/binding), get the viewModel, observe the "state", set the adapter in the recyclerView, and call the viewModel to start fetching data.
That's all.
How does it work then?
The app starts, the fragment is created, subscribes to the VM state LiveData, and triggers the Fetch of data.
The ViewModel calls the repo, which is empty (new), so makeItems is called the list now has items and cached in the repo's "source" list. The viewModel receives this list asynchronously (in a coroutine) and posts the LiveData state.
The fragment receives the state and posts (submit) to the Adapter to finally show something.
When you "click" on an Item, ViewHolder (which has a click listener) triggers the "call back" towards the fragment which receives a position, this is then passed onto the Viewmodel and here the data is mutated in the Repo, which again, pushes the same list, but with a different reference on the clicked item that was modified. This causes the ViewModel to push a new LIveData state with the same list reference as before, towards the fragment, which -again- receives this, and does adapter.submitList(...).
The Adapter asynchronously calculates this and the UI updates.
It works, I can put all this in GitHub if you want to have fun, but my point is, while the concerns about the AsyncDiffer are valid (and may be or been true), this doesn't seem to be my (super limited) experience.
Are you using this differently?
When I tap on any row, the change is propagated from the Repository
UPDATE: forgot to include the doNothing function:
val doNothing: Unit
get() = Unit
I've used this for a while, I normally use it because it reads better than XXX -> {} to me. :)
While doing
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
you are creating a new list but items remains the same. You have to store that new list into a variable or passing that operation directly as a param to postItem.
I 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.
I have a fragment showing a list of items, observing from view model (from a http service, they are not persisted in database). Now, I need to delete one of those items. I have a delete result live data so the view can observe when an item has been deleted.
Fragment
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
viewModel.deleteItemLiveData.observe(viewLifecycleOwner) {
when (it.status) {
Result.Status.ERROR -> showDeletingError()
Result.Status.SUCCESS -> {
itemsAdapter.remove(it.value)
commentsAdapter.notifyItemRemoved(it.value)
}
}
}
}
fun deleteItem(itemId: String, itemIndex: Int) = lifecycleScope.launch {
viewModel.deleteItem(itemId, itemIndex)
}
ViewModel
val deleteItemLiveData = MutableLiveData<Result<Int>>()
suspend fun deleteItem(itemId: String, itemIndex: Int) = withContext(Dispatchers.IO) {
val result = service.deleteItem(itemId)
withContext(Dispatchers.Main) {
if (result.success) {
deleteItemLiveData.value = Result.success(itemIndex)
} else {
deleteItemLiveData.value = Result.error()
}
}
}
It is working fine, but the problem comes when I navigate to another fragment and go back again. deleteItemLIveData is emitted again with the last Result, so fragment tries to remove again the item from the adapter, and it crashes.
How con I solve this?
Rather than deleting an individual item from the adapter, it would make sense to update the original source of LiveData<List> since the view observes that list.
The item repository should handle deletions, removing that item from the LiveData<List> which in turns propagates the update to the view and then the adapter.
Repo might look something like this...
fun deleteItem(item: Item): Result {
val updated = items.value
updated.remove(item)
items.postValue(updated)
. . .
// propagate result of success/failure back to the view
}
fun observeItems() = items
In your fragment you would get immediate updates from a single LiveData source
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.observeItems().observe(viewLifecycleOwner) {
itemsAdapter.update(it) //use DiffUtil to update list or notifyDataSetChanged
}
}
}
Showing errors should be contextual, a toast message or some visual notification.
Update:
Handle error in deletion might look like this, off the top of my head...
suspend fun deleteItem(itemId: String, itemIndex: Int): Result = withContext(Dispatchers.IO) {
val result = service.deleteItem(itemId)
withContext(Dispatchers.Main) {
if (result.success) {
// push updated list to items
val updated = items.value
updated.remove(item)
items.postValue(updated)
Result.Success()
} else {
Result.error()
}
}
}
I found a solution. I changed my code so fragment observes from onCreate method instead of onViewCreated. And I changed the owner as well. Instead of viewLifecycleOwner now is this. This way, value is not re-emitted when fragment is resumed, but just when is created or viewModel.deleteItem is called specifically.
It is working properly now. If anybody considers this a bad solution, please, tell me.
It's a common problem when you use LiveData for events that should happen only one time. There are several solutions explained here and here. They either wrap the emitted data or the observers. In this wrapper they store a flag that tracks whether or not the event has been handled/emitted yet.