Home > Enterprise >  DiffUtil redraw all items in ListAdapter Kotlin
DiffUtil redraw all items in ListAdapter Kotlin

Time:11-12

I am using DiffUtil with ListAdapter in Android Kotlin. I am calling the data from the server in the onResume method. When onResume called every item whole data is redrawing the view. I want to update the view if any data change on the server side, so it will reflect in the app.

ListActivity.kt

class ListActivity : BaseActivity() {

    lateinit var binding: ListActivityLayoutBinding
    private val viewModel: ListViewModel by inject()
    private var listAdapter: listAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupViewModel()
        binding = ListActivityLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    private fun setupViewModel() {
        viewModel.liveData.observe(this, { list ->
            setupAdapter(list)
        })
    }

    private fun setupAdapter(list: List<XYZ>) {
        initializeAdapter()
        listAdapter?.submitList(list)
        binding.recyclerView.adapter = listAdapter
    }

    private fun initializeAdapter() {
        viewModel.abc?.let { abc ->
            listAdapter = ListAdapter(abc, object : Listener<XYZ> {
                override fun selectedItem(item: XYZ) {
                    // calling 
                    }
                }
            })
        } ?: run {
            Log.e("Error", "Error for fetching data")
        }
    }

    override fun onResume() {
        super.onResume()
        viewModel.fetchData()
    }
}

XYZ.kt

data class XYZ(
    val id: String? = null,
    val title: String? = null,
    val count: Int? = null,
    val status: String? = null,
    val item: Qqq? = null
)

QQQ.kt

data class Qqq(
    val id: String? = null,
    val rr: Rr? = null
)

Rr.kt

data class Rr(
    val firstName: String? = null,
    val lastName: String? = null,
)

ListAdapter.kt

class ListAdapter(
    private val abc: Abc,
    private val listener: Listener <XYZ>
) : ListAdapter<XYZ, ListViewHolder>(LIST_COMPARATOR) {

    companion object {
        private val LIST_COMPARATOR = object : DiffUtil.ItemCallback<XYZ>() {
            override fun areItemsTheSame(oldItem: XYZ, newItem: XYZ): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: XYZ, newItem: XYZ): Boolean {
                return ((oldItem.title == newItem.title) && (oldItem.status == newItem.status)
                        && (oldItem.count == newItem.count)
                        && (oldItem.item == newItem.item))
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
        return ListViewHolder.bindView(parent, abc)
    }

    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        holder.bindItem(getItem(position), listener)
    }
}

ListViewModel.Kt

class ListViewModel : BaseViewModel() {

    var abc: Abc? = null
    private var xyz: List<XYZ>? = null
    var liveData: MutableLiveData<List<XYZ>> = MutableLiveData()

    fun fetchData() {
        viewModelScope.launch {
          
            val firstAsync = async {
                if (abc == null) {
                    abc = getAbc() // First retrofit call
                }
            }
            val secondAsync = async {
                xyz = getXYZ() // Second retrofit call
            }
            firstAsync.await()
            secondAsync.await()
            liveData.postValue(xyz)
        }
    }
}

Note I want to check abc is not null in every call.

1. Is my DiffUitll Callback is correct?

2. First Initial call I want to redraw every item but, if I call viewModel.fetchData() in onResume if there are any changes that I need to do otherwise I don't want to redraw my whole list. Is there any suggestions?

CodePudding user response:

The reason why things flash on the screen is because you're creating a new instance of the Adapter every time you resume, and your viewModel subscriptions are retriggered.

Do not make the adapter late init, there's little benefit here.

class ListActivity : BaseActivity() {

    lateinit var binding: ListActivityLayoutBinding
    private val viewModel: ListViewModel by inject()

    private val adapterListener = object : Listener<XYZ> {
                override fun selectedItem(item: XYZ) { // TODO  }
            }
           

    private var listAdapter = ListAdapter(adapterListener)

Then set it when you can:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupViewModel()
        binding = ListActivityLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.yourRecyclerView.adapter = listAdapter
    }

And then once you observe data and you get it, call listAdapter.submitList(xxx). No need to recreate the adapter unless you really need a complete new adapter (why?).

    private fun setupViewModel() {
        viewModel.liveData.observe(this, { list ->
            listAdapter.submitList(list.toMutableList())
        })
    }

Regarding your "check" of abc

One thing initializeAdapter() function is needed to call every time because viewmodel.abc is checking every time that value is not null.

This is the ViewModel's problem. You should not push a new list, if the requirements aren't met. If the list you supply in setupViewModel via viewmodel.liveData.observe should not be there if viewmodel.abc is null, then you should not push the livedata yet or should push a different state.

The Fragment should react to the data it receives, but the processing of said data, and logic, belongs elsewhere (viewModel and deeper into use-cases/repos).

All your fragment does is construct the framework stuff (a RecyclerView, and its accessories, like Adapter, Layoutmanager if needed, etc.) and subscribes to a liveData flow that will provide the data needed to wire it with the Framework stuff. It doesn't do much "thinking" and shouldn't.

Update

Here is a Pull Request in your sample project that I threw in a couple of minutes. When I run that, I see the two mocked items on screen in the RecyclerView.

CodePudding user response:

This answer is all based on not changing your current ListAdapter design. If you could modify the design so the callback and Abc can be passed into properties at any time (so it has an empty constructor), everything would be simpler. You could create the adapter as a val property, and you could expose separate Abc and List<Xyz> LiveDatas instead of having to merge them.

The adapter should be created only once. You're creating a new Adapter each time you receive new data, which means all old views are thrown away and the new Adapter has to layout the views from scratch. Since your Adapter class can only be instantiated when an Abc is available, you should use a lazy strategy for instantiating it (only create one if it's null).

It seems like your Adapter is dependent on some information from Abc class before it can show the list. Then I would combine both into a single data class, and publish them together in the LiveData.

It also looks like you have no need to retrieve Abc more than once so you can use a lazy strategy for retrieving it, too.

data class AbcXyzData(abc: Abc, xyzList: List<Xyz>)

class ListViewModel : BaseViewModel() {

    private val mutableLiveData = MutableLiveData<AbcXyzData>()
    val liveData: LiveData<AbcXyzData> get() = mutableLiveData

    fun fetchData() {
        viewModelScope.launch {
            val xyzDeferred = async { getXYZ() }
            val abc = liveData.value?.abc ?: getABC() // assuming getABC() suspends
            mutableLiveData.value = AbcXyZData(abc, xyzDeferred.await())
        }
    }
}
class ListActivity : BaseActivity() {

    lateinit var binding: ListActivityLayoutBinding
    private val viewModel: ListViewModel by inject()
    private var listAdapter: ListAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupViewModel()
        binding = ListActivityLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    private fun setupViewModel() {
        viewModel.liveData.observe(this) { (abc, xyzList) ->
            initializeAdapter(abc)
            listAdapter?.submitList(xyzList)
        })
    }

    private fun initializeAdapter(abc: Abc) {
        if (listAdapter == null) {
            listAdapter = ListAdapter(abc, object : Listener<XYZ> {
                override fun selectedItem(item: XYZ) {
                    // calling 
                    }
                }
            })
            binding.recyclerView.adapter = listAdapter
        }
    }

    override fun onResume() {
        super.onResume()
        viewModel.fetchData()
    }
}
  • Related