I'm performing some operations in my model list to be passed after to recycler adapter and update the adapter list. For some unexpected reason the adapter list has been updated before i pass the new list. I'm using DiffUtil to calculate the difference between an oldList and newList.
internal fun CheckoutDetailsActivity.updateLineItemWithPartialPayment(lineItemModelToUpdate: LineItemModel){
GlobalScope.launch {
withContext(Dispatchers.Main) {
calculateTodayPayments(lineItemModelToUpdate)
}
}
}
internal suspend fun CheckoutDetailsActivity.calculateTodayPayments(lineItemModelToUpdate: LineItemModel){
var todayPayments: BigDecimal = BigDecimal.ZERO
val lineItems: ArrayList<LineItemModel> = ArrayList(viewModel.ticket?.lineItems)
withContext(Dispatchers.Default){
lineItems.forEachIndexed loop#{ index, lineItemModel ->
if(lineItemModel.id == lineItemModelToUpdate.id){
lineItems[index] = lineItemModelToUpdate
todayPayments += lineItemModelToUpdate.partialPaymentAmount?: BigDecimal.ZERO
return#loop
}
lineItemModel.inPartialPayment = true
todayPayments += lineItemModel.partialPaymentAmount?: BigDecimal.ZERO
}
}
viewModel.todayPayments = todayPayments
updateRecyclerAdapter(lineItems)
showTodayPaymentContainer()
}
internal fun CheckoutDetailsActivity.updateRecyclerAdapter(lineItems: ArrayList<LineItemModel>){
viewModel.lineItemsWithPartialPayment.clear()
viewModel.lineItemsWithPartialPayment.addAll(lineItems)
viewModel.recyclerViewAdapter?.submitList(viewModel.sortLineItems(ArrayList(lineItems)))
viewModel.ticket?.lineItems = lineItems
}
this is my submitList() function in the adapter:
fun submitList(lineItemList: List<LineItemModel>){
val oldLineItemList = lineItems
val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(
LineItemDiffCallback(
oldLineItemList,
lineItemList
)
)
this.lineItems.clear()
this.lineItems.addAll(lineItemList)
diffResult.dispatchUpdatesTo(this)
}
class LineItemDiffCallback(
var oldLineItemList: List<LineItemModel>,
var newLineItemList: List<LineItemModel>
): DiffUtil.Callback(){
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldLineItemList[oldItemPosition].id == newLineItemList[newItemPosition].id
}
override fun getOldListSize(): Int {
return oldLineItemList.size
}
override fun getNewListSize(): Int {
return newLineItemList.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldLineItemList[oldItemPosition] == newLineItemList[newItemPosition]
}
}
In the last method i'm updating the adapter list with submitList(), when i check the adapter list in viewModel.todayPayments = todayPayments line it has been already updated with the previous operation "forEachIndexed()", for that reason DiffUtil doesn't works correctly, because it can't find difference between oldList and newList.
updateLineItemWithPartialPayment calls calculateTodayPayments that calls updateRecyclerAdapter that calls updateLineItemWithPartialPayment that calls calculateTodayPayments that calls updateRecyclerAdapter [...] you get the point...
Why are using extension functions? Aren't you calling them from the activity itself?
Another thing... is this submitList function yours or are you using a PagedListAdapter?
Related
Currently, I am making a task in Android that changes the unit value of the list according to the toggle button and shows the list with the changed value.
I am observing the list using a ViewModel and LiveData.
So i use toList() to return a new list and overwrite the old list to observe the values.
However, the screen is not updated even though it has returned a new list.
I've tried debugging and I'm getting some incomprehensible results.
Obviously, the address values of the old list and the new list are different, but even the unit of the old list has changed.
What happened?
Even if the addresses of Lists are different, do the values of the old list and the new list change at the same time because the properties refer to the same place?
I'll show you the minimal code.
Fragment
// Change Unit
toggleButton.addOnButtonCheckedListener { _, checkedId, isChecked ->
if(isChecked) {
when(checkedId) {
R.id.kg -> vm.changeUnit("kg")
R.id.lb -> vm.changeUnit("lbs")
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.items.observe(viewLifecycleOwner) { newList ->
adapter.submitList(newList)
}
}
WorkoutSetInfo
#Entity(
foreignKeys = [
ForeignKey(
entity = Workout::class,
parentColumns = arrayOf("workoutId"),
childColumns = arrayOf("parentWorkoutId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class WorkoutSetInfo(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val set: Int,
var weight: String = "",
var reps: String = "",
var unit: String = "kg",
val parentWorkoutId: Long = 0
)
Adapter
class DetailAdapter
: ListAdapter<WorkoutSetInfo, DetailAdapter.ViewHolder>(DetailDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemRoutineDetailBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(currentList[position])
}
inner class ViewHolder(val binding: ItemRoutineDetailBinding) : RecyclerView.ViewHolder(binding.root) {
private var weightTextWatcher: TextWatcher? = null
private var repTextWatcher: TextWatcher? = null
fun bind(item: WorkoutSetInfo) {
binding.set.text = item.set.toString()
binding.weight.removeTextChangedListener(weightTextWatcher)
binding.unit.text = item.unit
binding.rep.removeTextChangedListener(repTextWatcher)
weightTextWatcher = object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
override fun afterTextChanged(w: Editable?) {
if(!binding.weight.hasFocus())
return
item.weight = w.toString()
}
}
repTextWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(r: Editable?) {
if(!binding.rep.hasFocus())
return
item.reps = r.toString()
}
}
binding.apply {
weight.setTextIfDifferent(item.weight)
weight.addTextChangedListener(weightTextWatcher)
rep.setTextIfDifferent(item.reps)
rep.addTextChangedListener(repTextWatcher)
}
}
}
}
DiffUtil*
class DetailDiffCallback : DiffUtil.ItemCallback<WorkoutSetInfo>() {
override fun areItemsTheSame(
oldItem: WorkoutSetInfo,
newItem: WorkoutSetInfo
): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(
oldItem: WorkoutSetInfo,
newItem: WorkoutSetInfo
): Boolean {
return oldItem == newItem
}
}
ViewModel
class DetailViewModel(application: Application, title: String) : ViewModel() {
private val workoutDao = DetailDatabase.getDatabase(application)!!.workoutDao()
private val repository: WorkoutRepository = WorkoutRepository(workoutDao, title)
private val _items: MutableLiveData<List<WorkoutSetInfo>> = MutableLiveData()
val items = _items
fun changeUnit(unit: String) {
repository.changeUnit(unit)
_items.postValue(repository.getList())
}
fun addSet() {
viewModelScope.launch(Dispatchers.IO){
repository.add()
_items.postValue(repository.getList())
}
}
fun deleteSet() {
repository.delete()
_items.postValue(repository.getList())
}
fun save() {
viewModelScope.launch(Dispatchers.IO) {
repository.save()
}
}
}
Repository
class WorkoutRepository(private val workoutDao : WorkoutDao, title: String) {
private val workout = Workout(title = title)
private val setInfoList = ArrayList<WorkoutSetInfo>()
fun changeUnit(unit: String) {
setInfoList.map { setInfo ->
setInfo.unit = unit
}
}
fun add() {
val item = WorkoutSetInfo(set = setInfoList.size + 1)
setInfoList.add(item)
}
fun delete() {
if(setInfoList.size != 0)
setInfoList.removeLast()
return
}
fun save() {
val workoutId = workoutDao.insertWorkout(workout)
val newWorkoutSetInfoList = setInfoList.map { setInfo ->
setInfo.copy(parentWorkoutId = workoutId)
}
workoutDao.insertSetInfoList(newWorkoutSetInfoList)
}
fun getList() : List<WorkoutSetInfo> = setInfoList.toList()
}
You'd need to post your observer code for any help with why it's not updating.
As for the weird behaviour, setInfoList contains a few WorkoutSetInfo objects, right? Let's call them A, B and C. When you call setInfoList.toList() you're creating a new container, which holds the same references to objects A, B and C. Because it's a separate list, you can add and remove items without affecting the original list, but any changes to the objects that both share will be reflected in both lists - because they're both looking at the same thing.
So when you do setInfoList.map { setInfo -> setInfo.unit = unit } (which should be forEach really, map creates a new list you're discarding) you're modifying A, B and C. So every list you've made that contains those objects will see those changes, including your old list.
Basically if you want each list to be independent, when you modify the list you need to create new instances of the items, which means copying your WorkoutSetInfo objects to create new ones, instead of updating the current ones. If it's a data class then you can do that fairly easily (so long as you don't have nested objects that need copying themselves):
// var so we can replace it with a new list
private var setInfoList = listOf<WorkoutSetInfo>()
fun changeUnit(unit: String) {
// create a new list, copying each item with a change to the unit property
setInfoList = setInfoList.map { setInfo ->
setInfo.copy(unit = unit)
}
}
You don't need to do toList() on getList anymore, since you're just passing the current version of the list, and that list will never change (because you'll just create a new one). Meaning you don't need that function, you can just make setInfoList public - and because I changed it to listOf which creates an immutable List, it's safe to pass around because it can't be modified.
The WorkoutSetInfo objects inside that list could still be modified externally though (e.g. by changing one of the items' unit value), so instead of making a new copy when you call changeUnit, you might want to do it when you call getList instead:
class WorkoutRepository(private val workoutDao : WorkoutDao, title: String) {
private val workout = Workout(title = title)
private val setInfoList = ArrayList<WorkoutSetInfo>()
// store the current unit here
private var currentUnit = "kg"
fun changeUnit(unit: String) {
currentUnit = unit
}
// return new List
fun getList() : List<WorkoutSetInfo> = setInfoList.map { it.copy(unit = currentUnit) }
}
Now everything that calls getList gets a unique list with unique objects, so they're all separate from each other. And if you don't actually need to store the current unit value, you could pass it in to getList instead of having a changeUnit function:
fun getList(unit: String) = setInfoList.map { it.copy(unit = unit) }
I am implementing filterable list for RecyclerView using ListAdapter with AsyncDifferConfig.Builder that implements Filterable. When searching and no result match, a TextView will be shown.
adapter.filter.filter(filterConstraint)
// Searched asset may not match any of the available item
if (adapter.itemCount <= 0 && adapter.currentList.isEmpty() && filterConstraint.isNotBlank())
logTxtV.setText(R.string.no_data)
else
logTxtV.text = null
Unfortunately the update of filter did not propagate immediately on adapter's count and list.
The adapter count and list is one step behind.
The TextView should be displaying here already
But it only shows after updating it back and the list is no longer empty at this point
I am not sure if this is because I am using AsyncDifferConfig.Builder instead of regular DiffCallback
ListAdapter class
abstract class FilterableListAdapter<T, VH : RecyclerView.ViewHolder>(
diffCallback: DiffUtil.ItemCallback<T>
) : ListAdapter<T, VH>(AsyncDifferConfig.Builder(diffCallback).build()), Filterable {
/**
* True when the RecyclerView stop observing
* */
protected var isDetached: Boolean = false
private var originalList: List<T> = currentList.toList()
/**
* Abstract method for implementing filter based on a given predicate
* */
abstract fun onFilter(list: List<T>, constraint: String): List<T>
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
return FilterResults().apply {
values = if (constraint.isNullOrEmpty())
originalList
else
onFilter(originalList, constraint.toString())
}
}
#Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
submitList(results?.values as? List<T>, true)
}
}
}
override fun submitList(list: List<T>?) {
submitList(list, false)
}
/**
* This function is responsible for maintaining the
* actual contents for the list for filtering
* The submitList for parent class delegates false
* so that a new contents can be set
* While a filter pass true which make sure original list
* is maintained
*
* #param filtered True if the list was updated using filter interface
* */
private fun submitList(list: List<T>?, filtered: Boolean) {
if (!filtered)
originalList = list ?: listOf()
super.submitList(list)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
isDetached = true
}
}
RecyclerView Adapter
class AssetAdapter(private val glide: RequestManager, private val itemListener: ItemListener) :
FilterableListAdapter<AssetDataDomain, AssetAdapter.ItemView>(DiffUtilAsset()) {
inner class ItemView(itemView: AssetCardBinding) : RecyclerView.ViewHolder(itemView.root) {
private val assetName = itemView.assetName
private val assetPrice = itemView.assetPrice
private val assetMarketCap = itemView.assetMarketCap
private val assetPercentChange = itemView.assetPercentChange
private val assetIcon = itemView.assetIcon
private val assetShare = itemView.assetShare
// Full update/binding
fun bind(domain: AssetDataDomain) {
with(itemView.context) {
assetName.text = domain.symbol ?: domain.name
bindNumericData(
domain.metricsDomain.marketDataDomain.priceUsd,
domain.metricsDomain.marketDomain.currentMarketcapUsd,
domain.metricsDomain.marketDataDomain.percentChangeUsdLast24Hours
)
if (!isDetached)
glide
.load(
getString(
R.string.icon_url,
AppConfigs.ICON_BASE_URL,
domain.id
)
)
.into(assetIcon)
assetShare.setOnClickListener {
itemListener.onRequestScreenShot(
itemView,
getString(
R.string.asset_info,
domain.name,
assetPercentChange.text.toString(),
assetPrice.text.toString()
)
)
}
itemView.setOnClickListener {
itemListener.onItemSelected(domain)
}
}
}
// Partial update/binding
fun bindNumericData(priceUsd: Double?, mCap: Double?, percent: Double?) {
with(itemView.context) {
assetPrice.text = getString(
R.string.us_dollars,
NumbersUtil.formatFractional(priceUsd)
)
assetMarketCap.text = getString(
R.string.mcap,
NumbersUtil.formatWithUnit(mCap)
)
assetPercentChange.text = getString(
R.string.percent,
NumbersUtil.formatFractional(percent)
)
AppUtil.displayPercentChange(assetPercentChange, percent)
if (NumbersUtil.isNegative(percent))
assetPrice.setTextColor(Color.RED)
else
assetPrice.setTextColor(Color.GREEN)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemView =
ItemView(
AssetCardBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: ItemView, position: Int) {
onBindViewHolder(holder, holder.absoluteAdapterPosition, emptyList())
}
override fun onBindViewHolder(holder: ItemView, position: Int, payloads: List<Any>) {
if (payloads.isEmpty() || payloads[0] !is Bundle)
holder.bind(getItem(position)) // Full update/binding
else {
val bundle = payloads[0] as Bundle
if (bundle.containsKey(DiffUtilAsset.ARG_PRICE) ||
bundle.containsKey(DiffUtilAsset.ARG_MARKET_CAP) ||
bundle.containsKey(DiffUtilAsset.ARG_PERCENTAGE))
holder.bindNumericData(
bundle.getDouble(DiffUtilAsset.ARG_PRICE),
bundle.getDouble(DiffUtilAsset.ARG_MARKET_CAP),
bundle.getDouble(DiffUtilAsset.ARG_PERCENTAGE)
) // Partial update/binding
}
}
// Required when setHasStableIds is set to true
override fun getItemId(position: Int): Long {
return currentList[position].id.hashCode().toLong()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
isDetached = true
}
override fun onFilter(list: List<AssetDataDomain>, constraint: String): List<AssetDataDomain> {
return list.filter {
it.name.lowercase().contains(constraint.lowercase()) ||
it.symbol?.lowercase()?.contains(constraint.lowercase()) == true
}
}
interface ItemListener {
fun onRequestScreenShot(view: View, description: String)
fun onItemSelected(domain: AssetDataDomain)
}
}
UPDATE:
I can confirm that using DiffCallback instead of AsyncDifferConfig.Builder does not change the behavior and issue. It also seems that currentList is in async thus update on list does not reflect immediately after calling submitList.
I do not know if this is intended behavior but upon overriding onCurrentListChanged the currentList parameter is working correctly.
But the adapter.currentList is behaving like a previousList parameter
When you submit a list to recyclerView, it takes some time to compare items of current list and the previous one (to see if an item is removed, moved or added). so the result is not immediately ready.
you can use a RecyclerView.AdapterDataObserver to be notified of changes in recyclerView (it will tell what happened to items overall, like 5 were added etc)
P.S. if you look at recyclerView source code you will see that the DiffCallBack passed in the constructor, is wrapped in AsyncDifferConfig
I wanted to add a SearchView to my recyclerview. I wanted it to be at the top and scrollable with the items. To achieve this, I created separate adapter for my header and it contains the Searchview as well. Then I used a ConcatAdapter to combine this header adapter with the contents below it.
Initially I want all the items to be visible under the SearchView from _onBoardingState which is a MutableStateFlow and when user searches for a tag then the results for it get added to _onSearch which is also a MutableStateFlow.
I have this MutableStateFlow, _onBoardingState inside my ViewModel that gets the value from Firestore in the init of ViewModel. The number of results is less (~ 20) so there is no pagination implemented and all items get loaded at once.
Now, whenever user wants to search an item by a tag, the SearchView returns a Flow of the typed value and also a Flow that updates about if the SearchView is still open or closed. I used these extension functions for this:
fun SearchView.getQueryTextChangeStateFlow(onSubmit: ()-> Unit): StateFlow<String> {
val query = MutableStateFlow("")
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
onSubmit()
return true
}
override fun onQueryTextChange(newText: String): Boolean {
query.value = newText
return true
}
})
return query
}
fun SearchView.getActiveStateFlow(): StateFlow<Boolean> {
val isOpen = MutableStateFlow(false)
setOnSearchClickListener {
isOpen.value = true
}
setOnCloseListener {
isOpen.value = false
false
}
return isOpen
}
Inside my ViewModel I have
...
private val _onBoardingState: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _onSearch: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _isActive: MutableStateFlow<Boolean> = MutableStateFlow(false)
fun toggleSearchViewState(isActive: Boolean) {
_isActive.value = isActive
}
val cuurentFlow: Flow<Model?> =
_isActive.flatMapLatest { isActive ->
if (isActive) {
_onSearch
} else {
_onBoardingState
}
}
...
Now the issue here is, whenever the recyclerview is scrolled down, the SearchView gets recycled and hence the setOnCloseListener gets called for it. This causes the _isActive value to be set to false by the Header's Adapter so the value of cuurentFlow gets toggled which should not be happening.
I thought of a solution as to set the setOnCloseListener of SearchView inside the header adapter's onViewRecycled() to null, but this didn't help. Below is code for my Header Adapter as well if needed.
class OnBoardingHeaderAdapter(
private val context: Context,
) : RecyclerView.Adapter<OnBoardingHeaderAdapter.HeaderViewHolder>() {
private var queryTextListener: ((StateFlow<String>) -> Unit)? = null
private var searchViewListener: ((StateFlow<Boolean>) -> Unit)? = null
inner class HeaderViewHolder(binding: OnboardingHeaderItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val root = binding.headerRoot
val search = binding.search
fun bind(headerMetaData: HeaderMetaData) {
root.visibility =
if (headerMetaData.shouldShow)
View.VISIBLE
else
View.GONE
val searchEditText: EditText =
search.findViewById(androidx.appcompat.R.id.search_src_text)
searchEditText.setHintTextColor(context.resources.getColor(R.color.white))
searchEditText.setTextColor(context.resources.getColor(R.color.white))
}
}
fun setQueryTextListener(listener: (StateFlow<String>) -> Unit) {
this.queryTextListener = listener
}
fun setSearchViewListener(listener: (StateFlow<Boolean>) -> Unit) {
this.searchViewListener = listener
}
private val RECYCLER_COMPARATOR = object : DiffUtil.ItemCallback<HeaderMetaData>() {
override fun areItemsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem == newItem
}
val headerDiffer = AsyncListDiffer(this, RECYCLER_COMPARATOR)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val binding = OnboardingHeaderItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return HeaderViewHolder(binding)
}
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
//holder.setIsRecyclable(false)
if (position < 1) {
val header = headerDiffer.currentList[position]
holder.bind(header)
}
queryTextListener?.let {
it(holder.search.getQueryTextChangeStateFlow() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
val view: View = holder.search
imm.hideSoftInputFromWindow(view.windowToken,0)
})
}
searchViewListener?.let {
it(holder.search.getActiveStateFlow())
}
}
override fun getItemCount(): Int = headerDiffer.currentList.size
override fun onViewRecycled(holder: HeaderViewHolder) {
super.onViewRecycled(holder)
holder.search.setOnCloseListener(null)
}
}
I wanted to know what is the best approach to solve this issue, I think even if i use a recyclerview with multiple view types here for the header then still the recycling issue will be there.
I've been stuck trying to figure out how to update the list that my RecyclerView is showing.
What I'm trying to do is show a subset of a shown list when a spinner is changed. I have a collection of animals in my database and some have their pet attribute set as true and others have it set as false.
Using Room Database with repositories and viewModels, and what I've been trying to piece together is that it's good to have three different lists that I can tune into, so in m
Repository:
class AnimalRepository(private val animalDao: AnimalDao) {
val allAnimals: Flow<List<Animal>> = animalDao.getAnimalsByCategory()
val pets: Flow<List<Animal>> = animalDao.getAnimalsByPetStatus(true)
val nonPets: Flow<List<Animal>> = animalDao.getAnimalsByPetStatus(false)
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(animal: Animal) {
animalDao.insert(animal)
}
#WorkerThread
suspend fun get(id: Int): Animal {
return animalDao.get(id)
}
#WorkerThread
suspend fun delete(id: Int) {
animalDao.delete(id)
}
}
ViewModel
class AnimalViewModel(private val repository: AnimalRepository) : ViewModel() {
var allAnimals: LiveData<List<Animal>> = repository.allAnimals.asLiveData()
val pets: LiveData<List<Animal>> = repository.pets.asLiveData()
val nonPets: LiveData<List<Animal>> = repository.nonPets.asLiveData()
var result: MutableLiveData<Animal> = MutableLiveData<Animal>()
var mode: VIEW_MODES = VIEW_MODES.BOTH
/*
* Launching a new coroutine to insert the data in a non-blocking way
* */
fun insert(animal: Animal) = viewModelScope.launch {
repository.insert(animal)
}
/*
* Launching a new coroutine to get the data in a non-blocking way
* */
fun get(id: Int) = viewModelScope.launch {
result.value = repository.get(id)
}
fun delete(id: Int) = viewModelScope.launch {
repository.delete(id)
}
}
class AnimalViewModelFactory(private val repository: AnimalRepository) : ViewModelProvider.Factory {
override fun <T: ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AnimalViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AnimalViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
In my MainActivity I have it set up where I have an observer on these three lists and depending on which view mode is active (the spinner sets the view mode), that list is fed into the my RecyclerView's ListAdapter's submitList
animalViewModel.allAnimals.observe(this) { animals ->
if (viewMode == VIEW_MODES.BOTH) {
animals.let {
adapter.submitList(it)
// recyclerView.adapter = adapter
}
}
}
animalViewModel.pets.observe(this) { animals ->
if (viewMode == VIEW_MODES.PETS) {
animals.let {
adapter.submitList(it)
// recyclerView.adapter = adapter
}
}
}
animalViewModel.nonPets.observe(this) { animals ->
if (viewMode == VIEW_MODES.NON_PETS) {
animals.let {
adapter.submitList(it)
}
}
}
I am changing the mode with my spinner doing
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> {
viewMode = VIEW_MODES.BOTH
}
1 -> {
viewMode = VIEW_MODES.PETS
}
2 -> {
viewMode = VIEW_MODES.NON_PETS
}
}
adapter.notifyDataSetChanged()
}
This works fine if add or remove an animal after changing the view mode since the observers fire and the correct one is allowed to populate the adapter, but the notifyDataSetChanged() isn't doing anything and I've been stuck on getting the adapter to update without having to add or remove from the lists
I also tried resetting the adapter in the observer but that didn't do anything either
I am extremely new to kotlin and android programming, and I'm sure that I'm going about this the wrong way, but is there a way force a list refresh?
Update:
I think I may have found a found a solution but I worry that it's hacky. In my ViewModel I am replacing the contents of my allAnimals with the filtered lists
fun showBoth() {
allAnimals = repository.allAnimals.asLiveData()
}
fun showPets() {
allAnimals = repository.pets.asLiveData()
}
fun showNonPets() {
allAnimals = repository.nonPets.asLiveData()
}
and then in my main activity I changed my logic on when handling the spinner change to tell the view model to do its thing and then to remove the observer and slap it back on
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> {
animalViewModel.showBoth()
}
1 -> {
animalViewModel.showPets()
}
2 -> {
animalViewModel.showNonPets()
}
}
refreshObserver()
}
private fun refreshObserver() {
animalViewModel.allAnimals.removeObservers(this)
animalViewModel.allAnimals.observe(this) { animals ->
animals.let {
adapter.submitList(it)
}
}
}
this seems to work to get the recycler view to update, but is it hacky?
As far as I can see it makes perfect sense that notifyDataSetChanged isn't doing anything, you don't submit any new data before that call. However I think what you're trying to do is to get the adapter to react to a change in viewMode.
If this is the case, I would recommend also having your viewMode as a LiveData object and then expose a single list for your adapter to observe, which changes depending on the viewMode selected.
The Transformations.switchMap(LiveData<X>, Function<X, LiveData<Y>>) method (or its equivalent Kotlin extension function) would probably do most of the work for you here. In summary it maps the values of one LiveData to another. So in your example, you could map your viewMode to one of the allAnimals, pets and nonPets.
Here is a simple pseudocode overview for some clarity:
AnimalViewModel {
val allAnimals: LiveData<List<Animal>>
val pets: LiveData<List<Animal>>
val nonPets: LiveData<List<Animal>>
val modes: MutableLiveData<VIEW_MODES>
val listAnimals = modes.switchMap {
when (it) {
VIEW_MODES.BOTH -> allAnimals
...
}
}
}
fun onItemSelected {
viewModel.onModeChanged(position)
}
viewModel.listAnimals.observe {
adapter.submitList(it)
}
I have endless scroll in my recyclerview, so, it will update when there is new data. and i am using DiffUtil to update data in the recyclerview. DiffUtil does updates the data but whenever there is update data, recyclerview scroll to top and what it looks like is "using the notifydatasetchanged()". here is my DiffUtil and my adapter to update data.
class ProductDiffUtil(
val oldProductList: List<ProductModel>, val newProductList: List<ProductModel>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldProductList[oldItemPosition].id == newProductList[newItemPosition].id
}
override fun getOldListSize(): Int = oldProductList.size
override fun getNewListSize(): Int = newProductList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldProductList[oldItemPosition] == newProductList[newItemPosition]
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
return super.getChangePayload(oldItemPosition, newItemPosition)
}
}
Here is my adapter to update data
fun addProductList(productList: List<ProductModel>?) {
val diffResult = DiffUtil.calculateDiff(ProductDiffUtil(this.productList, productList!!))
this.productList.addAll(productList)
diffResult.dispatchUpdatesTo(this)
}
please help me with this. it is working fine when i am using notifyItemRangeChanged()... so what should i use to update data in recyclerview for best practice.
https://drive.google.com/open?id=1SapXW2wusmTpyGCRA9fa0aSLCYNL1fzN
You're comparing the previous contents against only the new items, rather than against the list with all of them added.
Imagine if this.productList is currently 1,2,3, and the new productList is 4,5,6. When you run
DiffUtil.calculateDiff(ProductDiffUtil(this.productList, productList!!)
It will compare 1 to 4, 2 to 5, etc. and conclude that everything has changed and no new items have been added. (note: this is an oversimplification of the DiffUtil algorithm, but serves to illustrate the point)
Instead, if you want to use DiffUtil:
val oldList = ArrayList(productList)
this.productList.addAll(productList)
val diffResult = DiffUtil.calculateDiff(ProductDiffUtil(oldList, productList!!)
diffResult.dispatchUpdatesTo(this)
or, since you know exactly how many items are added and where, just use notifyItemRangeInserted and avoid the copy:
val oldSize = this.productList.size
this.productList.addAll(productList)
notifyItemRangeInserted(oldSize, productList.size)
Consider making a generic diffUtil class instead of creating it for each adapter.
fun <T>diffList(oldList: List<T>, newList: List<T>, sameItem: (a: T, b: T) -> Boolean): DiffUtil.DiffResult {
val callback: DiffUtil.Callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return sameItem(oldList[oldItemPosition], newList[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
return DiffUtil.calculateDiff(callback) }
You can use it in your adapter like this:
fun setItems(products: List<Product>) {
val oldList = productList
productList = products
diffList(oldList, products, sameItem = { a, b -> a.id == b.id }).dispatchUpdatesTo(this)
}
Check if the layout manager has already been set and get the current scroll position. Like this:
var itemPostion= 0
if(myRecyclerView.layoutmanager != null){
itemPostion = (myRecyclerView.layoutmanager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition()
}
You can have a look at this sample project on GitHub