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()
}
}