I'm trying to use the realm data store for Android and i'm trying to build an application that shows the user a list of options in a ListView, kind of like the users contact list. The first letter for each section (such as A, B, C, etc.) should be a header. Is there a way to achieve this with RealmBaseAdapter?
Currently I have it working with ArrayAdapter and I simply have an array with values populated but would like to pull data from Realm using its adapter if possible. I know in iOS this is fairly straight forward using the NSFetchedResultsController. How do we break up the RealmResults into sections?
RealmBaseAdapter doesn't work with ExpandableListAdapter (which I am assuming you are using for sections?), so right now your only choice is creating your own implementation. But a RealmResults is also a List so it should work seamlessly with an ArrayAdapter.
You can also see more details here: https://github.com/realm/realm-java/issues/978
This is what I use:
package com.poterion.android.library.adapters
import android.widget.BaseExpandableListAdapter
import io.realm.*
/**
* #author Jan Kubovy <jan#kubovy.eu>
*/
abstract class RealmExpandableListAdapter<out Group : Any, Item : RealmModel>(
private val itemGroupsProvider: (Item) -> Collection<Group?>,
private val groupsProvider: (Collection<Item>) -> List<Group?>,
private var adapterData: OrderedRealmCollection<Item>?) : BaseExpandableListAdapter() {
private val listener: RealmChangeListener<OrderedRealmCollection<Item>>?
protected val groups: List<Group?>
get() {
return adapterData?.takeIf { isDataValid }?.let(groupsProvider) ?: emptyList()
}
private val isDataValid: Boolean
get() = adapterData?.isValid == true
init {
if (adapterData?.isManaged == false)
throw IllegalStateException("Only use this adapter with managed list, for un-managed lists you can just use the BaseAdapter")
this.listener = RealmChangeListener { notifyDataSetChanged() }
adapterData?.takeIf { isDataValid }?.also { addListener(it) }
}
private fun addListener(data: OrderedRealmCollection<Item>) {
when (data) {
is RealmResults<Item> -> data.addChangeListener((listener as RealmChangeListener<RealmResults<Item>>))
is RealmList<Item> -> data.addChangeListener((listener as RealmChangeListener<RealmList<Item>>))
else -> throw IllegalArgumentException("RealmCollection not supported: " + data.javaClass)
}
}
private fun removeListener(data: OrderedRealmCollection<Item>) {
when (data) {
is RealmResults<Item> -> data.removeChangeListener((listener as RealmChangeListener<RealmResults<Item>>))
is RealmList<Item> -> data.removeChangeListener((listener as RealmChangeListener<RealmList<Item>>))
else -> throw IllegalArgumentException("RealmCollection not supported: " + data.javaClass)
}
}
override fun getGroupCount(): Int = groups.size
override fun getChildrenCount(groupPosition: Int): Int = adapterData?.takeIf { isDataValid }?.let { data ->
val g = groups[groupPosition]
data.filter { g == null || groups(it).contains(g) }.size
} ?: 0
override fun getGroup(groupPosition: Int): Group? = if (groups.size > groupPosition) groups[groupPosition] else null
override fun getChild(groupPosition: Int, childPosition: Int): Item? = children(groupPosition)
.takeIf { it.size > childPosition }?.get(childPosition)
override fun notifyDataSetChanged() {
super.notifyDataSetChanged()
}
private fun children(groupPosition: Int): List<Item> {
return getGroup(groupPosition)
?.let { g -> adapterData?.takeIf { isDataValid }?.filter { groups(it).contains(g) } } ?: emptyList()
}
}
And usage:
class PersonListAdapter(realm: Realm) :
RealmExpandableListAdapter<String, Person>(
itemGroupsProvider = { person -> arrayOf(person.group, null) },
groupsProvider = { people -> people.map { it.group } },
adapterData = realm.where(Person::class.java)
.findAllSortedAsync("lastName", Sort.ASCENDING, "firstName", Sort.ASCENDING)) {
override fun getGroupId(groupPosition: Int) = getGroup(groupPosition).id
override fun getChildId(groupPosition: Int, childPosition: Int) = getChild(groupPosition, childPosition).id
override fun hasStableIds() = true
override fun getGroupView(groupPosition: Int, isExpanded: Boolean, convertView: View?, parent: ViewGroup?): View {
// ... Item View here ...
}
override fun getChildView(groupPosition: Int, childPosition: Int, isLastChild: Boolean,
convertView: View?, parent: ViewGroup?): View {
// ... Group View here ...
}
override fun isChildSelectable(groupPosition: Int, childPosition: Int) = true
}
Related
What I would want to do:
I would want to filter through list and show values that match the search phrase. Additionally I would want to show correct current list size in the UI.
What is the issue:
The issue is that I can filter through list, but on UI, my list size doesn't update. For example if I've downloaded 5 items to offline mode, it would show that there are still 5 items total, but there would be only 2 for example (and only 2 visible on the screen).
The next issue is that if I try to empty the search bar, the list doesn't go back to it's initial state. It's just empty and list size on UI shows that there are 5 items.
What I've tried:
I've tried adding notifyDataSetChanged() in adapter, but it doesn't work as intended. While debugging, The list is filtered and list after filtering is smaller, but it doesn't emit that value to the fragment.
Adapter:
class OssOfflineDevicesListAdapter(
private val offlineDevices: MutableList<OssOfflineDevicesI> = mutableListOf(),
private val removeDevicesFromQueue: (Long) -> Unit,
private val retryDownloadingDevices: (Long) -> Unit
) : RecyclerView.Adapter<OssOfflineDevicesListAdapter.OssOfflineDevicesListItemViewHolder>() {
private val filteredDevices: MutableList<OssOfflineDevicesI> = offlineDevices
override fun getItemCount(): Int = filteredDevices.size
fun filter(searchPhrase: String) {
val newOfflineDevices = offlineDevices.filter {
it.name().contains(searchPhrase, true)
}
updateListView(newOfflineDevices)
filteredDevices.clear()
filteredDevices.addAll(newOfflineDevices)
notifyDataSetChanged()
}
fun update(newValues: List<OssOfflineDevicesI>) {
updateListView(newValues)
filteredDevices.clear()
filteredDevices.addAll(newValues)
notifyDataSetChanged()
}
private fun updateListView(newValues: List<OssOfflineDevicesI>) {
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int = filteredDevices.size
override fun getNewListSize(): Int = newValues.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return filteredDevices[oldItemPosition].id() == newValues[newItemPosition].id()
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldDevices = filteredDevices[oldItemPosition]
val newDevices = newValues[newItemPosition]
return oldDevices.externalId() == newDevices.externalId() &&
oldDevices.downloadingStatus() == newDevices.downloadingStatus() &&
oldDevices.name() == newDevices.name()
}
}).dispatchUpdatesTo(this)
}
Fragment:
class OssOfflineDevicesListFragment : CoreFragment() {
private val disposableBag = CompositeDisposable()
private val viewModel by viewModel<OssOfflineDevicesListViewModel>()
private val offlineDevicesListAdapter = OssOfflineDevicesListAdapter(
removeDevicesFromQueue = { devicesExternalId -> removeDevicesFromQueue(devicesExternalId) },
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpUI()
observeOssActionTransmitter()
setUpQuickSearch()
viewModel.offlineDevices().observe(viewLifecycleOwner, { offlineDevices ->
if (offlineDevices.isNullOrEmpty()) {
showEmptyView()
} else {
showContentView(offlineDevices)
}
})
}
private fun setUpQuickSearch() {
search.searchEdit
.textChanges()
.skipInitialValue()
.skip(1, TimeUnit.SECONDS)
.debounce(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
offlineDevicesListAdapter.filter("$it")
}, {
offlineDevicesListAdapter.filter("")
})
.addTo(disposableBag)
}
private fun showEmptyView() {
recycler_view.gone()
empty_state_list_info.visible()
empty_list_state_image.visible()
updateResultCount(0)
}
private fun showContentView(offlineDevices: List<OssOfflineDevicesI>) {
empty_state_list_info.gone()
empty_list_state_image.gone()
offlineDevicesListAdapter.update(offlineDevices)
recycler_view.visible()
updateResultCount(offlineDevices.size)
}
private fun updateResultCount(resultCount: Int) {
search.countText.text = String.format("%s %d",
com.comarch.fsm.android.core.extensions.getString("total_results"), resultCount)
}
}
I have an activity that has a recyclerview. Each item of the recyclerview has 3 components: spinner, EditText and an ImageButton
In the activity, there's an "add users" button that should save all the info in the recycleViewer to DB. To give more context, there's also an "add user" button that adds another item to the recycleViewer.
The problem is that when I call the saveUsers function from the activity, who calls the newUserAdapter.getUsers(), that function always returns the list of items of the RecycleView empty (the editText and the Spinner) even if the user has modified the info on the recycleView
Here is my activity
class AddUser : AppCompatActivity() {
private lateinit var binding: ActivityAddUserBinding
private lateinit var newUserAdapter : AddUserRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddUserBinding.inflate(layoutInflater)
newUserAdapter = AddUserRecyclerViewAdapter()
val b = User("","")
newUserAdapter.addItem(b)
binding.recyclerViewUser.adapter = newUserAdapter
setContentView(binding.root)
}
fun addNewUserRow(view: View) {
Timber.i("AddUser addNewUserRow called")
val b = User("","")
newUserAdapter.addItem(b)
}
fun saveUsers(view: View) {
if(newUserAdapter.itemCount > 0)
{
var usr = newUserAdapter.getUsers()
//TODO: save usr to DataBase
}
else
{
Snackbar.make(view, R.string.delete_user, Snackbar.LENGTH_LONG).show()
}
}
}
This is my User data class:
data class User (
var name: String = String(),
var department: String = String()
)
And my RecycleViewAdapter:
class AddUserRecyclerViewAdapter : RecyclerView.Adapter<AddUserRecyclerViewAdapter.AddUserViewHolder>() {
private var allUsers = ArrayList<User>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddUserViewHolder {
Timber.i("User onCreateViewHolder")
val view = AddUserItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val tempUser = AddUserViewHolder(view)
tempUser.deleteBtn.setOnClickListener {
Timber.i("NewUser setOnClickListener " + tempUser.bindingAdapterPosition)
if (this.itemCount > 1)
{
deleteItem(tempUser.bindingAdapterPosition)
}
else
{
//At least one item, we don't let delete the last one
Snackbar.make(it, R.string.delete_user, Snackbar.LENGTH_LONG).show()
}
}
return tempUser
}
override fun onBindViewHolder(holderUser: AddUserViewHolder, position: Int) {
Timber.i("NewUser onBindViewHolder $position")
val item = allUsers[position]
holderUser.nameEt.setText(item.name.toString())
}
private fun deleteItem(pos: Int) {
Timber.i("NewUser deleteItem $pos")
allUsers.removeAt(pos)
// call notifyDataSetChanged() to notify our adapter.
notifyItemRemoved(pos)
}
fun addItem(item: User) {
allUsers.add(item)
// call notifyDataSetChanged() to notify our adapter.
notifyItemInserted(allUsers.size -1)
}
fun getUsers(): ArrayList<User> {
return allUsers
}
override fun getItemCount(): Int {
Timber.i("NewUser getItemCount called" + allUsers.size)
return allUsers.size
}
inner class AddUserViewHolder(binding: AddUserItemBinding) :
RecyclerView.ViewHolder(binding.root) {
var nameEt: EditText = binding.etName
var deleteBtn : ImageButton = binding.btnDelete
var spinnerDepartment : Spinner = binding.spinnerDepartment
}
}
Edit
As it seems that someone downvoted for lack of information, let me say it in different words. This is the function in RecycleViewAdapter:
fun getUsers(): ArrayList<User> {
return allUsers
}
Who is called when a button is clicked from the activity:
fun saveUsers(view: View) {
if(newUserAdapter.itemCount > 0)
{
var usr = newUserAdapter.getUsers()
//TODO: save usr to DataBase
}
else
{
Snackbar.make(view, R.string.delete_user, Snackbar.LENGTH_LONG).show()
}
}
So the var usr is where I get a list of empty users, where I expected to get a list of users with the information filled in the RecycleView.
You should update the property of the user in the ArrayList when you are changing the text with an addTextChangedListener
override fun onBindViewHolder(holderUser: AddUserViewHolder, position: Int) {
Timber.i("NewUser onBindViewHolder $position")
val item = allUsers[position]
holderUser.nameEt.setText(item.name.toString())
holderUser.nameEt.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0: Editable?) { }
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {
// Update user instance in allUsers
allUsers[position].name = text.toString()
}
})
}
You may want also want to define an onItemSelectedListener on the Spinner to update the selected department as well.
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'm trying to add some search on the RecyclerView list without using
notifyDataSetChanged()
instead to it using
diffutil.callback()
but the issue is that it change the list correctly but it doesn't change the UI correctly
Here is my code and I will explain it
class RecordsAdapter : RecyclerView.Adapter<RecordsAdapter.ViewHolder>() {
var adapterList = listOf<CustomerModel>()
var modelList = listOf<CustomerModel>()
set(value) {
adapterList = value
field = value
}
private var modelListFiltered = listOf<CustomerModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(CustomerCellBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(adapterList[position])
}
override fun getItemCount(): Int = adapterList.size
fun filter(isFiltered: Boolean, filterSearch: String) {
if (isFiltered) {
val filter = modelList
.filter {
it.name.contains(filterSearch) || it.id.contains(filterSearch)
}
modelListFiltered = filter
}
adapterList = if (isFiltered) modelListFiltered else modelList
val diff = CartDiffUtil(
if (isFiltered) modelList else modelListFiltered,
if (isFiltered) modelListFiltered else modelList
)
DiffUtil.calculateDiff(diff).dispatchUpdatesTo(this)
}
inner class ViewHolder(private var binding: CustomerCellBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(model: CustomerModel) {
binding.let {
it.model = model
it.executePendingBindings()
}
}
}
}
class CartDiffUtil(private val oldList: List<CustomerModel>, private val newList: List<CustomerModel>) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].id == newList[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]
}
So I'm calling filter function to filter and I'm sending two parameters on if there is any filter and the second is the search.
Now the issue appears in this scenario
0. searching ""
1. searching "testing 2"
2. searching "testing 4"
3. searching "testing 2"
4. searching ""
As you can see in the images, when I search for "testing 2" after "testing 4" it keeps showing "testing 4" and even if I clear the search it gives me two cells of "testing 4" instead of one "testing 2" and one "testing 4"
Hope my question is clear.
Thanks.
I'm guessing your juggling of three list properties is leading to some situations where there can be the same list instance in the before and after of the DiffUtil so it cannot successfully compare them.
Also, it's much easier to use ListAdapter instead of RecyclerView.Adapter when you want to use DiffUtil. Note that when you use ListAdapter, you use ItemCallback instead of Callback. ItemCallback is simpler.
Try doing it this way, where there is only the modelList and when it or the filter changes, you determine what the new list is and submit it to the ListAdapter and let it handle the changes.
class RecordsAdapter : ListAdapter<CustomerModel, RecordsAdapter.ViewHolder>(CustomerModelCallback) {
var modelList = listOf<CustomerModel>()
set(value) {
field = value
resync()
}
private var filterText: String = ""
private var isFiltered: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(CustomerCellBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(adapterList[position])
}
fun filter(isFiltered: Boolean, filterText: String = "") {
this.isFiltered = isFiltered
this.filterText = filterText
resync()
}
private fun resync() {
val newList = when {
isFiltered && filterText.isNotEmpty() ->
modelList.filter {
it.name.contains(filterSearch) || it.id.contains(filterSearch)
}
else -> modelList
}
submitList(newList)
}
// view holder...
}
object CustomerModelCallback : DiffUtil.ItemCallback<CustomerModel>() {
override fun areItemsTheSame(oldItem: CustomerModel, newItem: CustomerModel): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: CustomerModel, newItem: CustomerModel): Boolean =
oldItem == newItem
}
I'm having this issue, with recyclerView, may you check two screenshots below:
So that's my issue, when onNotifyItemChange runs, other info are changed, incorrectlty. Now here goes my adapter:
class TimelineAdapter(var timeline: TimelineDTO,
var toggleLikeClicked: OnRowClick,
var onCommentClicked: OnRowClick,
var onMediaClick: OnRowClick,
val onUserClicked: OnRowClick,
val reportPost: OnRowClick,
val editPost : OnRowClick,
val deletePost: OnRowClick,
val contract: TimelineViewContract) : BaseAdapter<RecyclerView.ViewHolder>() {
init {
setHasStableIds(true)
}
private var currentItem: Int = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (PostType.fromInt(viewType)) {
PostType.BASIC -> {
return PostViewHolder(parent.inflate(R.layout.row_post_default_item),
toggleLikeClicked, onCommentClicked, onMediaClick,
onUserClicked, reportPost,
editPost,
deletePost,
FirebaseAnalytics.getInstance(contract.returnContext()))
}
PostType.NEXT_TALKS -> {
return PostNextTalksViewHolder(parent.inflate(R.layout.row_post_next_talks_item),
contract)
}
else -> {
if(!BuildConfig.DEBUG) {
Crashlytics.log("Should not come here")
}
logE("adapter else!!")
return PostViewHolder(parent.inflate(R.layout.row_post_default_item),
toggleLikeClicked, onCommentClicked, onMediaClick,
onUserClicked, reportPost,
editPost,
deletePost,
FirebaseAnalytics.getInstance(contract.returnContext()))
}
}
}
override fun getItemCount(): Int {
var count = timeline.posts.size
if(hasValue(timeline.nextTalks.size)){
count++
}
return count
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
currentItem = position
val alignedPositon = getAlignedPosition(position)
when (holder) {
is PostViewHolder -> holder.bind(timeline.posts[alignedPositon])
is PostNextTalksViewHolder -> {
holder.bind(timeline.nextTalks)
}
is PostCarousselViewHolder -> {
holder.bind(ArrayList<String>())
}
}
}
fun getPostAt(position: Int): PostDTO {
val post: PostDTO
val alignedPositon = getAlignedPosition(position)
post = timeline.posts[alignedPositon]
return post
}
override fun getItemId(position: Int): Long {
val aligned = getAlignedPosition(position)
return aligned.toLong()
}
private fun getAlignedPosition(position: Int): Int {
var alignedPositon = position
if (hasValue(timeline.nextTalks.size)){
alignedPositon--
}
return alignedPositon
}
override fun getItemViewType(position: Int): Int {
val hasPinned = timeline.posts.any { it.postType == PostType.PINNED.id }
if(hasPinned) {
if(position == 1 && timeline.nextTalks.any()){
return PostType.NEXT_TALKS.id
}
}
else {
if(position == 0 && timeline.nextTalks.any()){
return PostType.NEXT_TALKS.id
}
}
return timeline.posts[getAlignedPosition(position)].postType
}
fun updateItemAt(postLocal: PostLocal, commentIndexPost: Int) {
timeline.posts.removeAt(commentIndexPost)
timeline.posts.add(commentIndexPost, PostDTO(postLocal))
notifyItemChanged(commentIndexPost)
}
fun addItems(newPosts: TimelineDTO) {
timeline.posts.addAll(newPosts.posts)
timeline.nextTalks.addAll(newPosts.nextTalks)
notifyItemRangeInserted(itemCount, newPosts.posts.size)
}
fun resetItems(nextPosts: TimelineDTO) {
timeline.posts.clear()
timeline.nextTalks.clear()
timeline.posts.addAll(nextPosts.posts)
timeline.nextTalks.addAll(nextPosts.nextTalks)
notifyDataSetChanged()
}
fun removeAt(position: Int) {
timeline.posts.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, timeline.posts.size)
}
}
Using notifyItemChanged() might trigger "fading in and out" effect which is not necessarily desired (unless You use stable IDs or killed change animation in animator).
If You know what was changed in an item, it's better to use an update payload (see an example here) to partially update your ViewHolders without triggering full rebind.
Otherwise if list is relatively small and You don't know what changed, you can also use DiffUtil to help generate list of changes/change payloads "semi-automatically".