Property value changes in ViewModel before it is updated in RecyclerViewAdapter - android

What I have is a ViewModel, a Fragment and a RecyclerViewAdapter.
The use case is as follows:
User wants to change the name of one item in the RecyclerView.
His command is sent to the ViewModel.
ViewModel updates the item and postValue using LiveData
Fragment observes on the live property and sends a command to the RecyclerViewAdapter to update the list.
List is updated.
Snippet containing only methods that I checked are valid for this problem.
data class Name(
var firstName: String = "",
var lastName: String = "",
)
class NameListRecyclerViewAdapter : RecyclerView.Adapter<NameListRecyclerViewAdapter.ListViewHolder>() {
private var names: List<Name> = listOf()
fun setData(newList: List<Name>) {
names = newList
notifyDataSetChanged()
}
}
class NameListViewModel : ViewModel() {
private var names: MutableList<Name>? = null
private val _nameList = MutableLiveData<MutableList<Name>>()
val nameList: LiveData<MutableList<Name>>
get() = _nameList
fun changeFirstName(index: Int, name: String) {
names?.get(0)?.firstName = name // in this very moment the names property in NameListRecyclerViewAdapter is being changed
_nameList.postValue(names)
}
}
class NamesListFragment : Fragment() {
private lateinit var viewModel: NameListViewModel
private lateinit var recyclerViewAdapter: NameListRecyclerViewAdapter
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
viewModel = ViewModelProvider(
requireActivity(),
NameListViewModelFactory(requireActivity().applicationContext)
).get(NameListViewModel::class.java)
recyclerViewAdapter = NameListRecyclerViewAdapter()
recycler_view_layout.apply {
setHasFixedSize(true)
layoutManager =
LinearLayoutManager(context).apply {
orientation = LinearLayoutManager.VERTICAL
}
adapter = recyclerViewAdapter
}
viewModel.nameList.observe(
viewLifecycleOwner,
Observer {
val result = it ?: return#Observer
recyclerViewAdapter.setData(result)
}
)
}
}
For the first time, the name is changing as it should, but the second one is very peculiar.
The name (the property in the RecyclerViewAdapter) is updated not when it should (in step 5) but in step 3 - even before the new value is posted using liveData.
My theory is that its the same list in the ViewModel and the RecyclerViewAdapter, but I have no idea why is that, why one is not a copy of the other??
Update The problem seems to be solved when in the ViewModel i add .toMutableList() as follows:
class NameListViewModel : ViewModel() {
//...
fun changeFirstName(index: Int, name: String) {
names?.get(0)?.firstName = name
_nameList.postValue(names.toMutableList()) // instead of simple _nameList.postValue(names)
}
}
Does that mean that live data property has the exact same list and I have to make sure to copy it always?

Related

Retrieve Firebase data using Coroutines

I'm trying to rewrite my program and start using Kotlin Coroutines.
That is my function to retrieve a list of products for a given group. After debugging it looks like everything is correct.
class FirebaseRepository {
private val db = FirebaseFirestore.getInstance()
private val auth = FirebaseAuth.getInstance()
fun getCurrentUserId(): String{
return auth.currentUser!!.uid
}
suspend fun getLista(): MutableLiveData<List<Produkt>> {
val result = MutableLiveData<List<Produkt>>()
val lista = mutableListOf<Produkt>()
db.collection(Constants.GROUP)
.document("xGRWy21hwQ7yuBGIJtnA")
.collection("Przedmioty")
.orderBy("dataDodaniaProduktu", Query.Direction.DESCENDING)
.get().await().forEach {
val singleProdukt = it.toObject(Produkt::class.java)
singleProdukt.produktId = it.id
lista.add(singleProdukt)
result.postValue(lista)
}
return result
}
That is my ViewModel class:
class ListaViewModel: ViewModel() {
private val repository = FirebaseRepository()
var _produkty = MutableLiveData<List<Produkt>>()
val produkty : LiveData<List<Produkt>> = _produkty
init {
viewModelScope.launch {
_produkty = repository.getLista()
}
}
And finally in my fragment I'm trying to observe live data but looks like nothing is being passed to my adapter. What am I doing wrong?
class ListaFragment : Fragment(), ListaAdapter.OnItemClickListener {
private var _binding: FragmentListaBinding? = null
private val binding get() = _binding!!
private lateinit var recyclerView : RecyclerView
private lateinit var listAdapter : ListaAdapter
private val listaViewModel by viewModels<ListaViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentListaBinding.inflate(inflater, container, false)
recyclerView = binding.recyclerView
listAdapter = ListaAdapter(emptyList(), this)
recyclerView.adapter = listAdapter // Zapobiega "No adapter attached; skipping layout"
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
recyclerView.setHasFixedSize(true)
listaViewModel.produkty.observe(viewLifecycleOwner, Observer {
listAdapter = ListaAdapter(it, this)
}
return binding.root
}
Try replacing this:
val produkty : LiveData<List<Produkt>> = _produkty
with this
val produkty : LiveData<List<Produkt>> get() = _produkty
This way you'll have "getter" rather than "initializer". Initializer will compute its value once (to the empty live data) and after you reassign that var it won't change the value of your val.
The problem in your code lies in the fact that you're creating a new instance of your ListaAdapter class inside the observe() method, without notifying the adapter about the changes. That's the reason why you're getting no results in the adapter. To solve this, simply create a method inside your adapter class:
fun setProduktList(produktList: List<Produkt>) {
this.produktList = produktList
notifyDataSetChanged()
}
Then inside your observe() method, use the following line of code:
listaViewModel.produkty.observe(viewLifecycleOwner, Observer {
//listAdapter = ListaAdapter(it, this) //Removed
listAdapter.setProduktList(it) 👈
}

Cannot reassign variable inside observer viewmodel

So I created MVVM app in kotlin to fetch movies from TMDB api, using injections and coroutines.
My problem is that I cannot copy the list of returned movies into a new list I created or reassign any variables inside the livedata observer from the MainActivity the values of variables stays the same as they were after exit the scope.
MainActivity class:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private val viewModel:MoviesViewModel by lazy {
ViewModelProvider(this)[MoviesViewModel::class.java]
}
private lateinit var list: MutableList<Movies>
private var number:Int=1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
getData()
list
number
}
private fun getData(){
list= mutableListOf()
viewModel.getData(page = "1").observe(this#MainActivity,Observer{ item ->
item?.let { res ->
when (res.status) {
Status.SUCCESS -> {
var tmpList = item.data?.body()?.results
list= tmpList!!.toMutableList()
number+=1
}
Status.ERROR -> {
res.message?.let { Log.e("Error", it) }
}}}
})}}
ViewModel class:
class MoviesViewModel #ViewModelInject constructor(var repository: MoviesRepository): ViewModel() {
fun getData(page:String)= liveData(Dispatchers.IO){
emit(Resource.loading(data = null))
try {
emit(Resource.success(data=repository.getMovies(api_key = Constants.API_KEY,
start_year=Constants.START_YEAR, end_year = Constants.END_YEAR,page = page)))
}catch (e:Exception){
emit(e.message?.let { Resource.error(message = it, data = null) })
}
}
}
As you can see I tried to change the value of number and load the list into my new list but outside the scope the values returned to be what they were before.
Very thankful for anyone who can assist.
Update:
So I tried to initialized all the items inside the success case and it worked I guess there is no other way to change the values outside the scope.

Is the RecyclerView absolutely wrong when there is only one item?

I'm making a function that looks like an image.
Although not shown in the image, these items can be added or deleted through the button.
Item is composed of Header and Detail. The adapter also has HeaderAdapter and DetailAdapter respectively.
I'm using ConcatAdapter to make sure there are HeaderAdapter and DetailAdatper per group rather than the whole list.
Because I thought it would be more manageable than using multiview types in one adapter (purely my opinion).
But I have a question. HeaderAdapter.
As you can see from the image, there is one header per group. So, there must be only one HeaderItem in the HeaderAdapter of each group.
In this case, I don't think there is much reason to use the Adapter.
In my case, is it better to use a multiview type for one Adapter?
RoutineItem
sealed class RoutineItem(
val layoutId: Int
) {
data class Header(
val id: String = "1",
val workout: String = "2",
val unit: String = "3",
) : RoutineItem(VIEW_TYPE) {
companion object {
const val VIEW_TYPE = R.layout.routine_item
}
}
data class Detail(
val id: String = UUID.randomUUID().toString(), // UUID
val set: Int = 1,
var weight: String ="",
val reps: String = "1"
) : RoutineItem(VIEW_TYPE) {
companion object {
const val VIEW_TYPE = R.layout.item_routine_detail
}
}
}
HeaderAdapter
class HeaderAdapter(item: RoutineItem.Header) : BaseAdapter<RoutineItem.Header>(initialItem = listOf(item)) {
override fun createViewHolder(itemView: View): GenericViewHolder<RoutineItem.Header> {
return HeaderViewHolder(itemView)
}
override fun getItemCount(): Int = 1
class HeaderViewHolder(itemView: View) : GenericViewHolder<RoutineItem.Header>(itemView)
}
DetailAdapter
class DetailAdapter(private val items: List<RoutineItem.Detail> = emptyList())
: BaseAdapter<RoutineItem.Detail>(initialItem = items) {
override fun createViewHolder(itemView: View): GenericViewHolder<RoutineItem.Detail> {
return DetailViewHolder(itemView)
}
override fun getItemCount(): Int = items.size
class DetailViewHolder(itemView: View) : GenericViewHolder<RoutineItem.Detail>(itemView)
}
Activity
class MainActivity : AppCompatActivity() {
var concatAdpater: ConcatAdapter = ConcatAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rv: RecyclerView = findViewById(R.id.rv)
val adapterItems: ArrayList<Pair<RoutineItem.Header, List<RoutineItem.Detail>>> = arrayListOf()
val childItems : List<RoutineItem.Detail> = listOf(
RoutineItem.Detail(),
RoutineItem.Detail(),
RoutineItem.Detail(),
RoutineItem.Detail(),
RoutineItem.Detail()
)
adapterItems.add(Pair(RoutineItem.Header(), childItems))
adapterItems.add(Pair(RoutineItem.Header(), childItems))
adapterItems.add(Pair(RoutineItem.Header(), childItems))
for ((header, list) in adapterItems) { // 1 adapter per group
concatAdpater.addAdapter(HeaderAdapter(header))
concatAdpater.addAdapter(DetailAdapter(list))
}
rv.adapter = concatAdpater
}
}
Because it is a test code, there are parts that are not functionally implemented! (Ex. Dynamically adding and deleting items..)
It's always better to use a single adapter because item animation and state changes are way more managable with DiffUtil. Also it's easier to maintain and way more efficient (in terms of speed and resource managment).
More detailed answers:
https://stackoverflow.com/a/53117359/6694770
https://proandroiddev.com/writing-better-adapters-1b09758407d2

DiffUtil Not working in nested recyclerview Kotlin

I have two recycler views. My view is not updated until I used notifyDataSetChanged. I asked for a similar type of issue, but this time I have Github Link. So please have a look and explain to me what I am doing wrong. Thanks
MainActivity.kt
package com.example.diffutilexample
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.diffutilexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var groupAdapter: GroupAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.fetchData()
binding.button.setOnClickListener {
viewModel.addData()
}
}
private fun setupViewModel() {
viewModel.groupListLiveData.observe(this) {
if (groupAdapter == null) {
groupAdapter = GroupAdapter()
binding.recyclerview.adapter = groupAdapter
}
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
binding.recyclerview.post {
groupAdapter?.notifyDataSetChanged()
}
}
}
}
ActivityViewModel.kt
package com.example.diffutilexample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var groupListLiveData: MutableLiveData<Boolean> = MutableLiveData()
var groupList: ArrayDeque<Group>? = null
set(value) {
field = value
groupListLiveData.postValue(true)
}
var value = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
groupList = groupByData(response.abc)
}
}
private fun groupByData(abc: List<Abc>?): ArrayDeque<Group> {
val result: ArrayDeque<Group> = groupList ?: ArrayDeque()
abc?.iterator()?.forEach { item ->
val key = GroupKey(item.qwe)
result.addFirst(Group(key, mutableListOf(item)))
}
return result
}
fun addData() {
groupList?.let { lastList ->
val qwe = Qwe("Vivek ${value++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val lastGroup = lastList[0]
lastGroup.list.add(item)
groupList = lastList
}
}
}
Please find the whole code in Github Link. I attached in above
I haven't debugged this, but if you remove your overuse of MutableLists and vars, and simplify your LiveData, you will likely eliminate your bug. At the very least, it will help you track down the problem.
MutableLists and DiffUtil do not play well together!
For example, Group's list should be a read-only List:
data class Group(
val key: GroupKey,
val list: List<Abc?> = emptyList()
)
It's convoluted to have a LiveData that only reports if some other property is usable. Then you're dealing with nullability all over the place here and in the observer, so it becomes hard to tell when some code is going to be skipped or not from a null-safe call. I would change your LiveData to directly publish a read-only List. You can avoid nullable Lists by using emptyList() to also simplify code.
You can avoid publicly showing your interior workings with the ArrayDeque as well. And you are lazy loading the ArrayDeque unnecessarily, which leads to having to deal with nullability unnecessarily.
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
private val _groupList = MutableLiveData<List<Group>>()
val groupList: LiveData<List<Group>> get() = _groupList
private val trackedGroups = ArrayDeque<Group>()
private var counter = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
addFetchedData(response.abc.orEmpty())
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
private fun addFetchedData(abcList: List<Abc>) {
for (item in abcList) {
val key = GroupKey(item.qwe)
trackedGroups.addFirst(Group(key, listOf(item)))
}
}
fun addData() {
if (trackedGroups.isEmpty())
return // Might want to create a default instead of doing nothing?
val qwe = Qwe("Vivek ${counter++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val group = trackedGroups[0]
trackedGroups[0] = group.copy(list = group.list + item)
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
In your Activity, since your GroupAdapter has no dependencies, you can instantiate it at the call site to avoid dealing with lazy loading it. And you can set it to the RecyclerView in onCreate() immediately.
Because of the changes in ViewModel, observing becomes very simple.
If you do something in setupViewModel() that updates a view immediately, you'll have a crash, so you should move it after calling setContentView().
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private val groupAdapter = GroupAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).apply {
setContentView(root)
recyclerview.adapter = groupAdapter
button.setOnClickListener {
viewModel.addData()
}
}
setupViewModel()
viewModel.fetchData()
}
private fun setupViewModel() {
viewModel.groupList.observe(this) {
groupAdapter.submitList(it)
}
}
}
Your DiffUtil.ItemCallback.areItemsTheSame in GroupAdapter is incorrect. You are only supposed to check if they represent the same item, not if their contents are the same, so it should not be comparing lists.
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem.key == newItem.key
}
And in GroupViewHolder, you are creating a new adapter for the inner RecyclerView every time it is rebound. That defeats the purpose of using RecyclerView at all. You should only create the adapter once.
I am predicting that the change in the nested list is going to look weird when the view is being recycled rather than just updated, because it will animate the change from what was in the view previously, which could be from a different item. So we should probably track the old item key and avoid the animation if the new key doesn't match. I think this can be done in the submitList() callback parameter to run after the list contents have been updated in the adapter by calling notifyDataSetChanged(), but I haven't tested it.
class GroupViewHolder(val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
companion object {
//...
}
private val adapter = NestedGroupAdapter().also {
binding.nestedRecyclerview.adapter = it
}
private var previousKey: GroupKey? = null
fun bindItem(item: Group?) {
val skipAnimation = item?.key != previousKey
previousKey = item?.key
adapter.submitList(item?.list.orEmpty()) {
if (skipAnimation) adapter.notifyDataSetChanged()
}
}
}
Side note: your adapters' bindView functions are confusingly named. I would just make those into secondary constructors and you can make the primary constructor private.
class GroupViewHolder private constructor(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
constructor(parent: ViewGroup) : this(
ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
//...
}
I'm not entirely sure, and I admit I haven't extensively studied your code, and this is not a solution, but this might point you in the right direction of how to solve it.
The thing about
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
Is that toMutableList() does indeed make a copy of the list. But each of the objects in the list are not copies. If you add things to an object in the original list, like you do in addData() it in fact is also already added to the copy that is in the adapter. That's why a new submitList doesn't recognize it as a change because it is actually the same as it was before the submitList.
As far as I understand, working with DiffUtil works best if the list you submit only contains objects that are immutable, so mistakes like this can't happen. I have ran into a similar problem before and the solution is also not straightforward. In fact, I don't entirely remember how I solved it back then, but hopefully this pushes you in the right direction.

Why SavedStateHandle doesn't save or return my livedata?

I use Navigation Component and load data in a recyclerview (MVVM architecture so this fragment has a viewmodel) if I move to another fragment and then I navigate back to the fragment which has a viewmodel then the viewmodel livedata is empty but I don't know why.
If I know correctly I don't need to manually call savedStateHandle.set to persist the data because savedStateHandle does it anyway.
I store Place objects in the livedata and this class is extends Parcelable.
ViewModel:
class OverviewViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
companion object {
private const val SAVED_All_PLACES_LIVEDATA_KEY = "savedAllPlacesLiveDataKey"
}
private val repository: PlacesRepository = PlacesRepository(this)
private var allPlacesList: MutableList<Place> = mutableListOf()
var allPlacesLiveData: MutableLiveData<MutableList<Place>> = savedStateHandle.getLiveData<MutableList<Place>>(SAVED_All_PLACES_LIVEDATA_KEY)
fun getAllPlacesFromRepository(...)
repository.getAllPlaces(...) //when successfully gets the data then call the viewmodel's addAllPlacesLiveData function
}
fun addAllPlacesLiveData(sites: List<Site>) {
allPlacesList.clear()
val places = mutableListOf<Place>()
for(i in 0..9) {
val site = sites[i]
val distance = site.distance / 1000
val place = Place(site.name, site.formatAddress, distance)
places.add(place)
}
places.sortBy {
it.distance
}
allPlacesList.addAll(places)
allPlacesLiveData.value = allPlacesList
}
}
Place:
#Parcelize
data class Place(
var name: String = "",
var address: String = "",
var distance: Double = 0.0
) : Parcelable
I subscribe for the changes of the livedata and then call getAll:
overviewViewModel.allPlacesLiveData.observe(viewLifecycleOwner) { places ->
recyclerViewAdapter.submitList(places.toMutableList()) }
overviewViewModel.getAllPlacesFromRepository(...)
Could you show how you're initializing your VM inside your Fragment?
Make sure you send over the savedState like this:
private val viewModel: YourViewModel by viewModels {
SavedStateViewModelFactory(requireActivity().application, this)
}

Categories

Resources