live data observer triggered when another variable - android

sorry for my English
I just started learning android and I'm trying to make a library app that uses Room db, recView and fragments
in the app there are 3 lists, all books, already read and Wishlist - the already read and Wishlist are just books that its property isAlreadyRead is true
in the viewModel i created 3 lists members, one for each list
--Book class--
#Entity(tableName = "books_table")
data class Book(
#PrimaryKey(autoGenerate = true)
val id: Int = 1,
val name: String,
val author: String,
val desc: String,
val imgUrl: String,
#ColumnInfo(defaultValue = "0")
val isAlreadyRead: Boolean = false,
#ColumnInfo(defaultValue = "0")
val isWishlist: Boolean = false
)
--repository--
val allBooks = bookDao.getAllBooks().asLiveData()
val alreadyReadBooks = bookDao.getAlreadyReadBooks().asLiveData()
val wishlistBooks = bookDao.getWishlistBooks().asLiveData()
--viewModel--
val allBooks = repository.allBooks
val alreadyReadBooks = repository.alreadyReadBooks
val wishlistBooks = repository.wishlistBooks
(the repository and viewModel is just the important section of the code, if you need the whole code i will update the question)
the problem mainly occurs when i delete an object, i added the ability to delete using swipe with ItemTouchHelper.SimpleCallback
private val itemTouchHelperCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder !is BookRecViewAdapter.BookViewHolder) {
return
}
bookViewModel.removeFromWishlistBooks(viewHolder.bookId)
adapter.notifyDataSetChanged()
}
}
when i delete in the already books, the bookViewModel.alreadyReadBooks observer is being called, but i noticed that also the bookViewModel.allBooks observer is being called, here is the code of the observers
--observer code inside onViewCreated of the fragments--
bookViewModel.allBooks.observe(requireActivity(), Observer { allBooks ->
Log.d("observer:", "show all books observer")
// Update the cached copy of the allBooks in the adapter.
allBooks?.let { adapter.books = it }
if (allBooks.isNotEmpty()) {
binding.listEmptyTextView.visibility = View.INVISIBLE
} else {
binding.listEmptyTextView.visibility = View.VISIBLE
}
})
(the code for the already read fragment and the wishist fragment is the same just that i called bookViewModel.alreadyReadBooks)
it crashes the app because in the observer i also want to update the visibility of a textView which just say that the list if empty (so there will be no white screen with no information), but binding is null because the fragment is in the background (i think that is the reason)
if you need any more info or code say, first time writing in stack overflow
thanks in advance
BTW: if you have any advice for me i will be happy to hear

My guess is that you are using requireActivity() instead of viewLifecycleOwner when you observe the bookViewModel. If I didn't guess right, you should show us the code that you have in your Fragment class.

Related

How can DiffUtil know about list changes when only the companion object has changed?

As shown in the image, I would like the unit of the Detail item to be changed at once according to the toggle button.
Detail list items were set as companion objects because it was determined that it was not necessary to have a unit property individually.
However, it seems that DiffUtil determines that there is no change between the new list and the old list, perhaps because the unit property is set as a companion object.
So there is no update of the view either.
How can I make DiffUtil responsive while changing the companion object?
Detail
#Entity
data class Detail(
#PrimaryKey(autoGenerate = true)
var id: Int,
val set: Int,
var weight: String = "",
var reps: String = "") {
companion object {
var title: String = ""
var unit: String = "kg"
val memo = ""
}
}
ViewModel
class DetailViewModel(application: Application) : ViewModel() {
private val repository: DetailRepository
private val _items: MutableLiveData<List<Detail>> = MutableLiveData()
val items = _items
private val list: List<Detail>
get() = _items.value ?: emptyList()
init {
val detailDao = DetailDatabase.getDatabase(application)!!.detailDao()
repository = DetailRepository(detailDao)
}
fun changeUnit(unit: String) {
Detail.unit = unit
if(list == null)
return
_items.postValue(list) // To notify the observer.
}
fun addDetail() {
viewModelScope.launch(Dispatchers.IO){
val item = Detail(0, set = list.size+1)
repository.add(item)
// If use plus(), a new List is returned.
// Therefore, the change is notified to the Observer by postValue of the new list added.
_items.postValue(list.plus(item))
}
}
fun deleteDetail() {
// Delete the last set and return a new list to postValue to notify the Observer of the change.
_items.postValue(list.dropLast(1))
}
}
DiffUtil
class DetailDiffCallback : DiffUtil.ItemCallback<Detail>() {
override fun areItemsTheSame(
oldItem: Detail,
newItem: Detail
): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(
oldItem: Detail,
newItem: Detail
): Boolean {
return oldItem == newItem
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.items.observe(viewLifecycleOwner) { newList ->
adapter.submitList(newList)
}
}
Detail list items were set as companion objects because it was determined that it was not necessary to have a unit property individually.
This is the root of the problem. If you want DiffUtil to be able to "see" these changes, you will have to move this information out of the companion object.
DiffUtil works by taking in two instances of your class and doing work (the areItemsTheSame() and areContentsTheSame() methods) to see if anything has changed. Since this information is part of the companion object, it will always be identical for all instances, which means there's no way for DiffUtil to detect a change, even if one has happened.
Like Ben P says, when you change the value in the companion object, that affects the entire class (since they all share that object). It's not included in the generated equals() code for the data class - because why would it need to be? Every instance shares the same value, it's not part of the state!
Even if it were, when you compare oldList and newList, oldList has still been "updated" with the new unit value, because it's in that shared companion object. If you want oldList and newList to be able to have different values for unit, they need to be per-instance properties.
But the way you're doing things here, the unit doesn't even seem to be part of the data, right? It's not like you're storing a value and a unit of measure, and doing conversions when the displayed unit type changes. It looks like this is just a general display option, that just applies to how the data is displayed.
So in that case, why not just call notifyDataSetChanged() on the adapter, or something similar? Force a redraw, let it show the new unit type, that's it. I'm not sure if you need to do anything special if you're using DiffUtil, but that's what I'd look into.
(I feel like storing the unit type in the data would be a way better approach, seems important to what the stored number actually means, but for what you're doing right now, a display refresh should be enough)

Android LiveData/StateFlow List Item Property Update Issue

So I'm updating my RecylerView with StateFlow<List> like following:
My data class:
data class Student(val name: String, var isSelected: Boolean)
My ViewModel logic:
fun updateStudentsOnSelectionChanged(targetStudent: Student) {
val targetIndex = _students.value.indexOf(targetStudent)
val isSelected = !targetStudent.isSelected
_students.value[targetIndex].isSelected = isSelected //<- doesn't work
}
Problem: The UI is not changed, but the isSelected inside _student is changed, what's going on? (same to LiveData)
I assume _students is a StateFlow<List>. Changing the isSelected property of the Student model doesn't trigger the StateFlow. The workaround would be to make the isSelected property of the Student data class immutable to get it compared when new state is set, create a MutableList out of the current list and copy the existing Student object with the new value for isSelected property:
data class Student(val name: String, val isSelected: Boolean)
val students = _students.value.toMutableList()
students[targetIndex] = students[targetIndex].copy(isSelected = isSelected)
_students.value = students
Ok, thanks to #Tenfour04 and #Sergey, I finally found out that StateFlow/LiveData cannot detect the internal changes, that's because they are both actually comparing the Reference of the .value.
That means, If I want to force the StateFow<List> to update, the only way is to assign a new List to it, therefore I created the following helper extension function:
fun <T> List<T>.mapButReplace(targetItem: T, newItem: T) = map {
if (it == targetItem) {
newItem
} else {
it
}
}
//this function returns a new List with the content I wanted
In Fragment:
val itemCheckAction: (Student) -> Unit = { student ->
val newStudent = student.copy(isSelected = !student.isSelected) //prepare a new Student
viewModel.updateStudentsOnSelectionChanged(student, newStudent) //update the StateFlow
}
In ViewModel:
fun updateStudentsOnSelectionChanged(currentStudent: Student, newStudent: Student) {
val newList = _students.value.mapButReplace(currentStudent, newStudent)
_students.value = newList //assign a new List with different reference
}

DiffUtil not refreshing view in Observer call android kotlin

Hey I am using diff util with ListAdapter. The updating of list works but I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged(). I tried everything inside this answer ListAdapter not updating item in RecyclerView only working for me is notifyItemChanged or setting adapter again. I am adding some code. Please someone know how to fix this problem?
Data and Enum class
data class GroupKey(
val type: Type,
val abc: Abc? = null,
val closeAt: String? = null
)
data class Group(
val key: GroupKey,
val value: MutableList<Item?> = ArrayDeque()
)
enum class Type{
ONE,
TWO
}
data class Abc(
val qq: String? = null,
val bb: String? = null,
val rr: RType? = null,
val id: String? = null
)
data class RType(
val id: String? = null,
val name: String? = null
)
data class Item(
val text: String? = null,
var abc: Abc? = null,
val rr: rType? = null,
val id: String? = null
)
viewmodel.kt
var list: MutableLiveData<MutableList<Group>?> = MutableLiveData(ArrayDeque())
fun populateList(){
// logic to call api
list.postValue(data)
}
fun addItemTop(){
// logic to add item on top
list.postValue(data)
}
inside view model I am filling data by api call inside viewmodel function and return value to list. Also another function which item is inserting at top of list so that's why is used ArrayDeque
Now I am adding nested reyclerview diff util callback.
FirstAdapter.kt
class FirstAdapter :
ListAdapter<Group, RecyclerView.ViewHolder>(comp) {
companion object {
private val comp = object : DiffUtil.ItemCallback<Group>() {
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean {
return ((oldItem.value == newItem.value) && (oldItem.key == newItem.key))
}
}
}
......... more function of adapter
}
FirstViewHolder
val adapter = SecondAdapter()
binding.recyclerView.adapter = adapter
adapter.submitList(item.value)
SecondAdapter.kt
class SecondAdapter : ListAdapter<Item, OutgoingMessagesViewHolder>(comp) {
companion object {
private val comp = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return ((oldItem.rr == newItem.rr) &&
(oldItem.text == oldItem.text) && (oldItem.abc == newItem.abc))
}
}
}
..... more function
}
Activity.kt
viewModel.list.observe(this, { value ->
submitList(value)
})
private fun submitList(list: MutableList<Group>?) {
adapter?.submitList(list)
// adapter?.notifyDataSetChanged()
}
I am 100% sure that my list is updating and my observer is calling when my new list is added. I debug that through debug view. But problem is I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged()
UPDATE
viewmodel.kt
class viewModel : BaseViewModel(){
var list: MutableLiveData<MutableList<Group>?> = MutableLiveData()
//... more variables...
fun fetchData(context: Context) {
viewModelScope.launch {
val response = retroitApiCall()
response.handleResult(
onSuccess = { response ->
list.postValue(GroupData(response?.items, context))
},
onError = { error ->
Log.e("error" ,"$error")
}
)
}
}
}
internal fun GroupData(items: List<CItem>?, context: Context): MutableList<Group> {
val result: MutableList<Group> = MutableList()
items?.iterator()?.forEach { item ->
// adding item in list by add function and then return list.
return result
}
private fun addItemOnTop(text: String) {
list.value?.let { oldlist ->
// logic to add items on top of oldlist variable
if(top != null){
oldlist.add(0,item)
}else{
val firstGroup = oldlist[0]
firstGroup.value.add(item)
}
list.postValue(oldlist)
}
}
}
I am using sealed class something like this but not this one Example. And Something similar to these when call api Retrofit Example. Both link I am giving you example. What I am using in my viewmodel.
I don't know what's going on, but I can tell you two things that caught my attention.
First Adapter:
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
You're not comparing if the items are the same, you're comparing the items and their contents are the same. Don't you have an Id like you did in your second adapter?
I'd probably check oldItem.key == newItem.key.
Submitting the List
As indicated in the answer you linked, submitList has a very strange logic where it compares if the reference of the actual list is the same, and if it is, it does nothing.
In your question, you didn't show where the list comes from (it's observed through what appears to be liveData or RXJava), but the souce of where the list is constructed is not visible.
In other words:
// P S E U D O C O D E
val item1 = ...
val item2 = ...
val list1 = mutableListOf(item1, item2)
adapter.submitList(list1) // works fine
item1.xxx = ""
adapter.submitList(list1) // doesn't work well.
WHY?
Unfortunately, submitList's source code shows us that if the reference to the list is the same, the diff is not calculated. This is really not on the adapter, but rather on AsyncListDiffer, used by ListAdapter internally. It is this differ's responsibility to trigger the calculation(s). But if the list references are the same, it doesn't, and it silently ignores it.
My suspicion is that you're not creating a new list. This rather undocumented and silent behavior hurts more than it helps, because more often than not, developers aren't expecting to duplicate a list supplied to an object whose purpose and promise is to offer the ability to "magically" (and more importantly, automatically) calculate its differences between the previous.
I understand why they did it, but I would have at the very least emitted a log WARNING, indicating you're supplying the same list. Or, if you want to avoid polluting the already polluted logCat, then at least be much more explicit about it in its official documentation.
The only hint is this simple phrase:
you can use submitList(List) when new lists are available.
The key here being the word new lists. So not the same list with new items, but simply a new List reference (regardless of whether the items are the same or not).
What should you try?
I'd start by modifying your submitList method:
private fun submitList(list: MutableList<Group>?) {
adapter?.submitList(list.toMutableList())
}
For Java users out there:
adapter.submitList(new ArrayList(oldList));
The change is to create a copy of the list you receive: list.ToMutableList(). This way the AsyncListDiffer's check for list equality will return false and the code will continue.
UPDATE / DEBUG
Unfortunately, I don't know what is going on with your code; I assure you that ListAdapter works, as I use it myself on a daily basis; If you think you've found a case where there are problems with it, I suggest you create a small prototype and publish it on github or similar so we can reproduce it.
I would start by using debug/breakpoints in key areas:
ViewModel; write down the reference fromthe list you "return".
DiffUtil methods, is diffUtil being called?
Your submitList() method, is the list reference the same as the one you had in your ViewModel?
etc.
You need to dig a bit deeper until you find out who is not doing what.
On Deep vs Shallow copy and Java and whatever...
Please keep in mind, ListAdapter (through AsyncDiff) checks if the reference to the list is the same. In other words, if you have a list val x = mutableListOf(...) and you give this to the adapter, it will work the 1st time.
If you then modify the list...
val x = mutableListOf(...)
adapter.submitList(x)
x.clear()
adapter.submitList(x)
This will NOT WORK correctly, because to the eyes of the Adapter both lists are the same (they actually are the same list).
The fact that the list is mutable is irrelevant. (I still frown upon the mutable list; why does submitList accept a mutable list if you cannot mutate it and submit it again, escapes my knowledge but I would not have approved that Pull Request like so) It would have avoided most problems if they only took a non-mutable list, therefore implying you must supply a new list every time if you mutate it. Anyway...
as I was saying, duplicating a list is simple, in either Kotlin or Java there are multiple variations:
val newListWithSameContents = list1.toList()
List newListWithSameContents = ArrayList(list1);
now if list1 has an item...
list1.add("hello")
When you copy list1 into newList... The reference to "Hello" (the string) is the same. If String were mutable (it's not, but assume it is), and you modified that string somehow... you would be modifying both strings at the same time or rather, the same string, referenced in both lists.
data class Thing(var id: Int)
val thing = Thing(1)
val list1: MutableList<Thing> = mutableListOf(thing)
val list2: MutableList<Thing> = list1.toMutableList()
println(list1)
println(list2)
// This prints
[Thing(id=1)]
[Thing(id=1)]
Now modify the thing...
thing.id = 2
println(list1)
println(list2)
As expected, both lists, pointing to the same object:
[Thing(id=2)]
[Thing(id=2)]
This was a shallow copy because the items were not copied. They still point to the same thing in memory.
ListAdapter/DiffUtil do not care if the objects are the same in that regard (depending how you implemented your diffutil that is); but they certainly care if the lists are the same. As in the above example.
I hope this clarifies what is needed for ListAdapter to dispatch updates. If it fails to do so, then check if you're effectively doing the right thing.

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.

Kotlin : How to update a value of a RecyclerView

I want to update at any time some values in my RecyclerView.
Here is my data class ParameterText:
data class ParameterText(
var parameterName: String?,
var parameterValue: String?
)
Here is my ViewHolder class ParameterTextViewHolder:
class ParameterTextViewHolder(itemView: View) : ViewHolder(itemView) {
val parameterName: TextView = itemView.findViewById(R.id.parameterName)
val parameterText: TextView = itemView.findViewById(R.id.parameterValue)
}
Here is my Adapter (in my Activity):
// Adapter
private val parametersTextFoundList = emptyDataSourceTyped<ParameterText>()
And here is my RecyclerView setup (also in my Activity):
rv_parameters_text.setup {
withDataSource(parametersTextFoundList)
withItem<ParameterText, ParameterTextViewHolder>(R.layout.parameter_text) {
onBind(::ParameterTextViewHolder) { _, item ->
parameterName.text = item.parameterName
parameterText.text = item.parameterValue
}
}
}
I tried this:
private fun updateValue(index: Int, value: String) {
parametersTextFoundList[index].parameterValue = value
}
But it doesn't work. I read that I should also use the notifyDataSetChanged() method but I don't know where to use it. Can you help me?
There is an entire suite of notify API's, including notifyItemInserted(), notifyItemRemoved(), notifyItemChanged(), which are designed to more efficiently update a RecyclerView.
when changing the contents of one existing row in your RecyclerView, its more efficient to use adapter.notifyItemChanged(row), as notifyDataSetChanged() will reload the entire RecyclerView. I recommend:
private fun updateValue(index: Int, value: String)
{
parametersTextFoundList[index].parameterValue = value
rv_parameters_text.adapter?.notifyItemChanged(index)
}
You need to use notifyDataSetChanged() method with the update like this
rv_parameters_text.adapter?.notifyDataSetChanged()

Categories

Resources