I am trying to update my nested RecyclerView list using submitList in a LiveData Observer.
The list is submitted, but the UI only updates when you touch the screen.
The issue arose when I added nested RecyclerViews in my master RecyclerView.
So I think it has something to do with it.
I want it to update after the submitList is called.
What am I doing wrong?
Outer ListAdapter:
class ReservationAdapter(
private val changeState: (visit: Visit, newState: Visit.State) -> MutableLiveData<Visit?>
) : ListAdapter<Reservation, ReservationViewHolder>(ReservationDiffCallback()) {
private val recycledViewPool = RecyclerView.RecycledViewPool()
/// Filters \\\
// Building
var filterBuildingId = ""
set(value) {
field = value
filteredVisits = visits.filter(Visit.filter(value, filterSearchString))
}
// Search string
var filterSearchString = ""
set(value) {
field = value
filteredVisits = visits.filter(Visit.filter(filterBuildingId, value))
}
/// Filtering chain \\\
// Visits come in and get filtered
var visits = arrayListOf<Visit>()
set(value) {
field = value
filteredVisits = value.filter(Visit.filter(filterBuildingId, filterSearchString))
}
// Filtered visits come in, a reservation map is made
var filteredVisits = listOf<Visit>()
set(value) {
field = value
reservationVisits = Reservation.extractMapFromVisits(value)
}
// Reservation map comes in, and submits a list of reservations
private var reservationVisits = hashMapOf<Reservation, ArrayList<Visit>>()
set(value) {
field = value
_reservationVisits.value = value
submitList(value.keys.toMutableList())
notifyDataSetChanged()
}
private val _reservationVisits = MutableLiveData<HashMap<Reservation, ArrayList<Visit>>>()
/**
* Inflate items
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReservationViewHolder {
return ReservationViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.list_reservation,
parent,
false
),
recycledViewPool,
changeState,
_reservationVisits
)
}
/**
* Bind items
*/
override fun onBindViewHolder(holder: ReservationViewHolder, position: Int) {
val reservation = getItem(position)
holder.bind(reservation, reservationVisits[reservation])
}
}
Outer ViewHolder:
class ReservationViewHolder(
private val view: View,
private val recycledViewPool: RecyclerView.RecycledViewPool,
changeState: (visit: Visit, newState: Visit.State) -> MutableLiveData<Visit?>,
reservationVisits: MutableLiveData<HashMap<Reservation, ArrayList<Visit>>>
) : RecyclerView.ViewHolder(view) {
private val adapter = VisitAdapter(changeState)
private var _reservation: Reservation? = null
init {
/*
THIS IS WHERE I SETS THE VALUE, BUT DOESN'T UPDATE THE UI.
*/
reservationVisits.observe(view.context as LifecycleOwner) {
if (_reservation != null) {
(view.context as MainActivity).runOnUiThread {
adapter.submitList(null)
adapter.notifyDataSetChanged()
adapter.submitList(it[_reservation!!]?.toMutableList())
adapter.notifyDataSetChanged()
view.refreshDrawableState()
}
}
}
}
fun bind(reservation: Reservation, visits: ArrayList<Visit>?): View = with(view) {
println("3: ${visits?.size}")
_reservation = reservation
// Initialize values
txtSubject.text = reservation.subject
txtTime.text = "TIME"
// Recycler view for visits
rcvVisits.apply {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
layoutManager = LinearLayoutManager(context)
adapter = this#ReservationViewHolder.adapter
setRecycledViewPool(this#ReservationViewHolder.recycledViewPool)
}
// Add visits to adapter
adapter.submitList(visits)
// Return view
view
}
}
Inner ListAdapter:
class VisitAdapter(
private val changeState: (visit: Visit, newState: Visit.State) -> MutableLiveData<Visit?>
) : ListAdapter<Visit, VisitViewHolder>(VisitDiffCallback()) {
// Sorter
private val _visitSorter = compareBy<Visit>({ it.state }, { it.expectedArrival }, { it.visitor?.name })
// ListAdapter create
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VisitViewHolder {
return VisitViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.list_visitor,
parent,
false
),
changeState
)
}
// ListAdapter bind
override fun onBindViewHolder(holder: VisitViewHolder, position: Int) {
holder.bind(getItem(position))
}
// ListAdapter submit
override fun submitList(list: List<Visit>?) {
super.submitList(list?.sortedWith(_visitSorter) ?: listOf())
}
}
I checked your code these are what I found:
you created an instance of the adapter and an observer in view holder class.
view holder will instantiate multiple times, so you shouldn't instantiate the adapter in it. check this link
and if you want to pass data to your view holder, you should do it in the constructor, never observe for data change in view holder class.
so you can observe data in activity and pass data to your adapter.
and there is no need to call runOnUiThread in an observer. its already on main Thread (UI Thread).
The events are dispatched on the main thread. If LiveData already has data set, it will be delivered to the observer.
LiveData
I solved it.
The issue was with the com.mikepenz:aboutlibraries:7.0.4#aar library.
After I removed it and commented out any code that used it, it started working.
Related
I have a fragment where use creates a budget for a specific category like this:
Here is how it works: user adds a new budget item in NewBudgetFragment. That item gets displayed in BudgetFragment in recyclerview. Budget item has amountSpent variable that should be updated each time user adds a new transaction(this happens in another fragment). But after creating the budget item, if the user spends money on that specific category, the amountSpent doesn't get updated in the recyclerview item. I have used both LiveData and DiffUtil in the BudgetAdapter but I can't figure out why it doesn't get updated.
Here is BudgetAdapter:
class BudgetAdapter() : ListAdapter<Budget, BudgetAdapter.BudgetViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BudgetViewHolder {
val binding =
BudgetItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BudgetViewHolder(binding)
}
override fun onBindViewHolder(holder: BudgetViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem, position)
}
class BudgetViewHolder(val binding: BudgetItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(budget: Budget, position: Int) {
binding.apply {
tvBudgetName.text = budget.name
tvBudgetLimit.text = budget.limit.toString()
tvAmountSpent.text = budget.amountSpent.toString()
tvPercentageSpent.text = ((budget.amountSpent/budget.limit)*100).toInt().toString() + "%"
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Budget>() {
override fun areItemsTheSame(oldItem: Budget, newItem: Budget): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Budget, newItem: Budget): Boolean {
return oldItem == newItem
}
}
}
This is how new budget item gets created:
NewBudgetFragment:
...
viewModel.transactions.observe(viewLifecycleOwner) { it ->
transactionList = it.filter { it.category == listCategory[selectedCategoryIndex].name }
amountSpent = transactionList.sumOf { it.amount }
}
...
if (budgetName.isNotEmpty() && budgetLimit.isNotEmpty() && budgetCategory != null) {
viewModel.addBudget(
name = budgetName,
limit = budgetLimit.toDouble(),
amountSpent=amountSpent,
category = budgetCategory.name)
This is BudgetFragment.kt where the adapter is:
class BudgetFragment : Fragment(R.layout.fragment_budget),BudgetAdapter.OnItemClickListener {
private lateinit var binding: FragmentBudgetBinding
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentBudgetBinding.bind(view)
val budgetAdapter = BudgetAdapter(this)
val toolbar = binding.toolbar.root
toolbar.title = "Budget"
(requireActivity() as MainActivity).setSupportActionBar(toolbar)
binding.apply {
rvBudget.apply {
adapter = budgetAdapter
setHasFixedSize(true)
}
}
viewModel.budgets.observe(viewLifecycleOwner){
if(it.isNotEmpty()){
binding.rvBudget.visibility = View.VISIBLE
binding.tvNoBudget.visibility = View.INVISIBLE
}else{
binding.rvBudget.visibility = View.INVISIBLE
binding.tvNoBudget.visibility = View.VISIBLE
}
budgetAdapter.submitList(it)
}
binding.btAddBudget.setOnClickListener {
val action = BudgetFragmentDirections.actionBudgetFragmentToNewBudgetFragment()
findNavController().navigate(action)
}
}
Asuming that your Budget Class is a data class, then the content comparison should work, so we should not have any problems with the object itself that is being used by the Adapter and the DiffUtil.
But:
I can't see the ViewModel Code - are you submitting the same List Instance to the Adapter with submitList? For example, are you mutating the items in a private List in the ViewModel and posting the same List on the same Live Data everytime?
If yes, then this is probably the reason why the items in the RecyclerView are not being refreshed. You need to create a new Instance of the List, with the content of the old List and then post this on the LiveData.
Example ViewModel, if you don't want to override the behavior of "submitList" where you clear the previous data and add the new one and then call by yourself notifiyDatasetChanged()
class MyBudgetViewModel : ViewModel() {
// Important that its a data class, to actually have a content sensistive equals comparison
// if you don't have an ID, you have to work with list indexes when finding
// and updating this item
data class Budget(val id: Int, val amount: Double)
private val _budgets = MutableLiveData<List<Budget>>()
val budgets: LiveData<List<Budget>>
get() = _budgets
init {
_budgets.value = listOf(Budget(1, 20.0))
}
fun onBudgetChanged(id: Int, newBudget: Double) {
// depends on your setup and how you fill the initial list, this maybe should never be null
// by the time you call onBudgetChanged or something similar
val oldList = _budgets.value ?: emptyList()
// unused example variable - if you want to copy the list 1 by 1, ArrayList takes another list as Constructor
val newListUnchanged = ArrayList(oldList)
// map returns a new instance of the list.
val newList = oldList.map { oldItem ->
if (oldItem.id == id) {
oldItem.copy(amount = newBudget)
} else oldItem
}
_budgets.value = newList
}
}
The problem I am facing with the RecyclerView is the data is coming from Server and the API response is getting printed correctly in the console.
but when I am trying to set data in the adapter what is wrong or something is not going correctly with the flow that the data is not being updated on UI.
//This is my adapter class
class DashboardAdapter(val context: Context) : RecyclerView.Adapter<DashBoardHolder>() {
private var transactionList = ArrayList<DashboardData>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DashBoardHolder {
val inflator = LayoutInflater.from(parent.context)
val view = ActivityDashboardDataBinding.inflate(inflator, parent, false)
val viewHolder = DashBoardHolder(view)
return viewHolder
}
override fun onBindViewHolder(holder: DashBoardHolder, position: Int) {
val quickModel = transactionList[position]
holder.tvName.text = quickModel.bookingTitle
}
fun showListItems(dashboardlist: List<DashboardData>?, aboolean: Boolean) {
when {
aboolean -> transactionList.clear()
}
if (dashboardlist != null && !dashboardlist.isEmpty())
this.transactionList.addAll(dashboardlist)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return transactionList.size
}
}
MyHolderClass
class DashBoardHolder(val binding: ActivityDashboardDataBinding) :
RecyclerView.ViewHolder(binding.root) {
var tvName: TextView = binding.textViewGrandrukName
var tvTime: TextView = binding.tvGrandrukTripDetails
var tvPlace: ImageView = binding.btnGhandruk
var ivRectangle: ImageView = binding.imageView5
}
Similarly,I set the adapter in view section like this way:
//setting adapter in Presenter class
fun setAdapter() {
var layoutmanager: LinearLayoutManager? = LinearLayoutManager(appCompatActivity)
val firstVisiblePosition = layoutmanager!!.findFirstVisibleItemPosition()
binding!!.includesDashboardRecyclerview.rvBookingList.setHasFixedSize(true)
binding!!.includesDashboardRecyclerview.rvBookingList.layoutManager = layoutmanager
binding!!.includesDashboardRecyclerview.rvBookingList.adapter = dashboardAdapter
layoutmanager!!.scrollToPositionWithOffset(firstVisiblePosition, 0)
}
In Presenter Class, calling setAdapter class from presenter like this way
class DashboardPresenter(
private val dashboardView: DashboardView,
private val dashboardModel: DashboardModel
) {
fun onCreateView() {
onClick()
dashboardView.setAdapter()
getDashboardRequest()
}
//calling adpter function here
fun showList(termlist: List<DashboardData>?, aboolean: Boolean) {
(null as DashboardAdapter?)?.showListItems(termlist!!, aboolean)
}
}
I'm not able to understand what is getting wrong here.
(null as DashboardAdapter?)?.showListItems(termlist!!, aboolean)
You are calling showListItems() on the DashboardAdapter as a type not the instance dashboardAdapter. Assuming that dashboardAdapter is a local class field.
Also I guess this type casting is not necessary as you already using the optional ?
So, it can be simplified to:
dashboardAdapter?.showListItems(termlist!!, aboolean)
Assuming that this should be called whenever you retrieve the API response. So, showList() must be called when there's new API data.
My observer is working and it's getting called whenever a new record is entered,
The problem is with recycler view is only showing one record to begin with and it never updates itself to show additional records as a result of save setClickListener.
I can verify in the my database (Room with LiveData) that I've more than one record, there's some problem with the Adapter or the ViewHolder.
P.S. Plus any modifications to how things should be done when it comes to patterns with adapter are most welcome. I hope I got it right.
MainActivity onCreate method
viewModel = ViewModelProvider(this).get(BookViewModel::class.java)
save.setOnClickListener {
val book = Book(UUID.randomUUID().toString(), "author 2", "book 2")
viewModel.insert(book)
}
val bookListAdapter = BookListAdapter(this)
recyclerView.adapter = bookListAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
viewModel.allBooks.observe(this, Observer { books ->
books?.let {
Log.d(TAG, "onCreate: changed")
bookListAdapter.setBooks(books)
}
})
static classes in MainActivity
private class BookListAdapter(private val context: Context): RecyclerView.Adapter<BookListAdapter.BookViewHolder>() {
private var bookList: List<Book> = mutableListOf()
// getting called only once
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookListAdapter.BookViewHolder {
val itemView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
return BookViewHolder(itemView)
}
override fun onBindViewHolder(holder: BookListAdapter.BookViewHolder, position: Int) {
val book = bookList[position]
holder.setData(book.author, book.book, position)
}
override fun getItemCount(): Int {
Log.d("inner", "getItemCount: ${bookList.size}") // this return correct size
return bookList.size
}
fun setBooks(it: List<Book>?) {
bookList = it!!
notifyDataSetChanged()
}
private class BookViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
fun setData(a: String, b: String, p: Int) {
itemView.author.text = a
itemView.book.text = b
}
}
}
Based on this answer: How to update RecyclerView Adapter Data? I think that You have to notifyDataSetChanged from observer method not inside the adapter class.
viewModel.allBooks.observe(this, Observer { books ->
books?.let {
Log.d(TAG, "onCreate: changed")
bookListAdapter.setBooks(books)
bookListAdapter.notifyDataSetChanged()
}
})
fun setBooks(it: List<Book>) {
bookList = it
}
Also, when You use Room I think You can try to use DiffUtil. It will automatically refresh layout when it has to be refreshed and is much faster than calling notifyDataSetChanged every time Your data is changed.
I'm successfully passing data from MainActivity to my recyclerView via adapter, and my view with items is rendering correctly. However, I need to change one member of my item object on click (status), and i wrote a method for that (updateStatus), and it works great, it changes the value and save it to database.
But i cannot refresh my recyclerView, so it could render changed Status attribute. I need to go back on my phone, reenter, and then it renders it correctly. I have tried everything, from notifyDataSetChanged to restarting adapter, no luck. There is something missing and I can't find what.
Here is my MainActivity class
class MainActivity : AppCompatActivity() {
private var posiljkaDAO: PosiljkaDAO? = null
private var dostavnaKnjizicaDAO: DostavnaKnjizicaDAO? = null
private var allItems: ArrayList<DostavnaKnjizicaModel> = arrayListOf()
var adapter = RecycleViewAdapter(allItems)
private var eSifraPosiljke: EditText? = null
#RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_listview)
//get logo
supportActionBar!!.setDisplayShowHomeEnabled(true)
supportActionBar!!.setLogo(R.drawable.logo_bp)
supportActionBar!!.setDisplayUseLogoEnabled(true)
dostavnaKnjizicaDAO = DostavnaKnjizicaDAO(this)
dostavnaKnjizicaDAO?.closeDB()
getAllItems(this)
//connecting adapter and recyclerView
adapter = RecycleViewAdapter(allItems)
recycleView.adapter = adapter
recycleView.layoutManager = LinearLayoutManager(this)
recycleView.setHasFixedSize(true)
eSifraPosiljke = findViewById<EditText>(R.id.eSifraPosiljke)
posiljkaDAO = PosiljkaDAO(this)
}
//method that gets all items from database
private fun getAllItems(context: Context) {
var dostavenFromLOcal = dostavnaKnjizicaDAO?.getAllLocalDostavneKnjizice(context)
if (dostavenFromLOcal != null) {
allItems = dostavenFromLOcal
}
}
//method that changes status of an item
fun changeStatus(context: Context, IdDostavne: Int, statusDostavne: Int) {
dostavnaKnjizicaDAO = DostavnaKnjizicaDAO(context)
dostavnaKnjizicaDAO?.changeStatus(IdDostavne, statusDostavne)
getAllItems(context)
adapter.notifyDataSetChanged()
}
}
and my Adapter class
class RecycleViewAdapter(var dostavneKnjiziceBP: ArrayList<DostavnaKnjizicaModel>)
: RecyclerView.Adapter<RecycleViewAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view){
val nazivPrimaoca: TextView = view.txtNazivPrimaoca
val brojPosiljke: TextView = view.txtBrojPosiljke
val statusDostave: TextView = view.txtStatusDostave
val imgMore: ImageView = view.img_more
val context: Context = view.context
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutView = LayoutInflater.from(parent.context).inflate(R.layout.urucenje_posiljke_layout, parent, false)
return ViewHolder(layoutView)
}
override fun getItemCount() = dostavneKnjiziceBP.size
#RequiresApi(Build.VERSION_CODES.KITKAT)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
//New variable to get all modeliPosiljakaBP and their position
var dosKnjizica = dostavneKnjiziceBP[position]
val mainActivity = MainActivity()
//Sending data to layout for display in specific field
if (dosKnjizica.naziv_primaoca != null) {
holder.brojPosiljke.text = "${dosKnjizica.id_dostavna_knjizica}, "
holder.nazivPrimaoca.text = "${dosKnjizica.naziv_primaoca}"
if (dosKnjizica.naziv_primaoca!!.length > 25) {
holder.nazivPrimaoca.text = "${dosKnjizica.naziv_primaoca!!.subSequence(0, 25)}..."
}
} else {
holder.brojPosiljke.text = "${dosKnjizica.id_dostavna_knjizica}"
holder.nazivPrimaoca.text = ""
}
holder.statusDostave.text = "${dosKnjizica.status_dostave_naziv}"
when (dosKnjizica.status_dostave) {
StatusDostaveEnum.Neurucena.value -> {
holder.statusDostave.setTextColor(Color.RED)
}
StatusDostaveEnum.Uruceno.value, StatusDostaveEnum.ZaRejon.value, StatusDostaveEnum.Nadoslano.value, StatusDostaveEnum.Izgubljeno.value -> {
holder.statusDostave.setTextColor(Color.GREEN)
}
StatusDostaveEnum.Obavjesteno.value, StatusDostaveEnum.ZaNarednuDostavu.value -> {
holder.statusDostave.setTextColor(Color.BLUE)
}
StatusDostaveEnum.Retour.value -> {
holder.statusDostave.setTextColor(Color.parseColor("#dda0dd"))
}
}
//Calling menu menu_pregled_drugih_vrsta_posiljke to display menu options on click on three dots
holder.imgMore.setOnClickListener {
val popupMenu = PopupMenu(holder.context, it, Gravity.START)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.uruci -> {
//calling new activity from second item in dropdown menu
holder.imgMore.context.startActivity(
Intent(holder.imgMore.context, MainActivityInfo::class.java).putExtra(
"Id", dosKnjizica.id_dostavna_knjizica.toString()
)
)
true
}
//here i am calling my changeStatus method from MainActivity
R.id.obavjesti -> {
mainActivity.changeStatus(holder.context, dosKnjizica.id_dostavna_knjizica!!, StatusDostaveEnum.Uruceno.value)
Toast.makeText(holder.context, "obavjesti", Toast.LENGTH_SHORT).show()
true
}
R.id.vrati -> {
Toast.makeText(holder.context, "vrati", Toast.LENGTH_SHORT).show()
true
}
else -> false
}
}
popupMenu.inflate(R.menu.menu_urucenje_posiljke)
popupMenu.show()
}
}
}
Your adapter doesn't have the updated data. Initially, you fetch all data from the database and create an adapter with it: adapter = RecycleViewAdapter(allItems). Afterwards, you are updating the database, calling getAllItems(Context) but you don't pass the data to the adapter.
Add the line adapter.dostavneKnjiziceBP = allItems to the changeStatus method like this:
//method that changes status of an item
fun changeStatus(context: Context, IdDostavne: Int, statusDostavne: Int) {
dostavnaKnjizicaDAO = DostavnaKnjizicaDAO(context)
dostavnaKnjizicaDAO?.changeStatus(IdDostavne, statusDostavne)
getAllItems(context)
adapter.dostavneKnjiziceBP = allItems
adapter.notifyDataSetChanged()
}
Save dostavneKnjiziceBP as a private var inside the adapter and create functions for assigning and updating that ArrayList from within the adapter, using notifyDataSetChanged() everytime a change is done.
class RecycleViewAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<RecycleViewAdapter.ViewHolder>() {
private var items = ArrayList<DostavnaKnjizicaModel>()
// ...
internal fun setItems(items: ArrayList<DostavnaKnjizicaModel>) {
this.items = items
notifyDataSetChanged()
}
override fun getItemCount() = this.items.size
}
Also, try using adapter.notifyItemChanged(updateIndex); if you know the index of the updated item.
Been using realm and it's awesome.
Came up against something. Wondering if I'm doing something wrong.
I have a RealmRecyclerViewAdapter that I'm using to show the results of a realm query. This works perfectly if I add or update records in the realm. I had to setHasFixedSize(false) on the recycler view to get it to update on the fly. Not sure if this is correct but it worked.
Anyway, that's not my issue.
I'm experimenting with filtering my data. I have the following query:
realm.where(Person::class.java).contains("name", nameFilter, Case.INSENSITIVE).findAllSorted("name")
I'm passing this RealmResults to my recycler view and it works great on add/update.
However, when I attempt a filter, it doesn't update automatically.
Am I right in saying that simply changing my filter (specified by nameFilter) isn't enough for the query to be re-run? This would be fair enough I suppose. Since I guess there's no trigger for realm to know I've changed the value of the string.
However, even if I recalculate my query, it doesn't seem to update in the Recycler View unless I explicitly call updateData on my adapter. I'm not sure if this is the best or most efficient way to do this. Is there a better way?
Complete Code:
Main Activity
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val TAG: String = this::class.java.simpleName
private val realm: Realm = Realm.getInstance(RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build())
private var nameFilter = ""
private var allPersons: RealmResults<Person> = realm.where(Person::class.java).contains("name", nameFilter, Case.INSENSITIVE).findAllSorted("name")
private val adapter: PersonRecyclerViewAdapter = PersonRecyclerViewAdapter(allPersons)
private lateinit var disposable: Disposable
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
realm.executeTransaction({
// realm.deleteAll()
})
Log.i(TAG, "Deleted all objects from Realm")
buttonAddOrUpdatePerson.setOnClickListener(this)
setUpRecyclerView()
disposable = RxTextView.textChangeEvents(editTextNameFilter)
// .debounce(400, TimeUnit.MILLISECONDS) // default Scheduler is Computation
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith<DisposableObserver<TextViewTextChangeEvent>>(getSearchObserver())
}
private fun getSearchObserver(): DisposableObserver<TextViewTextChangeEvent> {
return object : DisposableObserver<TextViewTextChangeEvent>() {
override fun onComplete() {
Log.i(TAG,"--------- onComplete")
}
override fun onError(e: Throwable) {
Log.i(TAG, "--------- Woops on error!")
}
override fun onNext(onTextChangeEvent: TextViewTextChangeEvent) {
nameFilter = editTextNameFilter.text.toString()
allPersons = realm.where(Person::class.java).contains("name", nameFilter, Case.INSENSITIVE).findAllSorted("name")
// this is necessary or the recycler view doesn't update
adapter.updateData(allPersons)
Log.d(TAG, "Filter: $nameFilter")
}
}
}
override fun onDestroy() {
super.onDestroy()
realm.close()
}
override fun onClick(view: View?) {
if(view == null) return
when(view) {
buttonAddOrUpdatePerson -> handleAddOrUpdatePerson()
}
}
private fun handleAddOrUpdatePerson() {
val personToAdd = Person()
personToAdd.name = editTextName.text.toString()
personToAdd.email = editTextEmail.text.toString()
realm.executeTransactionAsync({
bgRealm -> bgRealm.copyToRealmOrUpdate(personToAdd)
})
}
private fun setUpRecyclerView() {
recyclerViewPersons.layoutManager = LinearLayoutManager(this)
recyclerViewPersons.adapter = adapter
recyclerViewPersons.setHasFixedSize(false)
recyclerViewPersons.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
}
}
PersonRecyclerViewAdapter
internal class PersonRecyclerViewAdapter(data: OrderedRealmCollection<Person>?, autoUpdate: Boolean = true) : RealmRecyclerViewAdapter<Person, PersonRecyclerViewAdapter.PersonViewHolder>(data, autoUpdate) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.person_row, parent, false)
return PersonViewHolder(itemView)
}
override fun onBindViewHolder(holder: PersonViewHolder?, position: Int) {
if(holder == null || data == null) return
val personList = data ?: return
val person = personList[position]
holder.bind(person)
}
internal class PersonViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var textViewName: TextView = view.findViewById(R.id.textViewNameDisplay)
var textViewEmail: TextView = view.findViewById(R.id.textViewEmailDisplay)
internal fun bind(person: Person) {
textViewEmail.text = person.email
textViewName.text = person.name
}
}
}
Yeah, updateData() is the way to do it. Since you updated the query, the Results you want to show becomes a different object. updateData() has to be called to notify the adapter that the data source is changed.
However, you may lose the nice animation for the RecyclerView in this way since the whole view will be refreshed because of the data source is changed. There are some ways to work around this.
eg.: You can add one field isSelected to Person. Query the results by isSelected field and pass it to the adaptor:
allPersons = realm.where(Person::class.java).equalTo("isSelected", true).findAllSorted("name")
adapter = PersonRecyclerViewAdapter(allPersons)
When changing the query:
realm.executeTransactionAsync({
var allPersons = realm.where(Person::class.java).equalTo("isSelected", true).findAllSorted("name")
for (person in allPersons) person.isSelected = false; // Clear the list first
allPersons = realm.where(Person::class.java).contains("name", nameFilter, Case.INSENSITIVE).findAllSorted("name") // new query
for (person in allPersons) person.isSelected = true;
})
It depends on your use case, if the list to show is long, this approach might be slow, you could try to add all the filtered person to a RealmList and set the RealmList as the data source of the adapter. RealmList.clear() is a fast opration than iterating the whole results set to set the isSelected field.
If the filter will mostly cause the whole view gets refreshed, updateData() is simply good enough, just use it then.