I have two recycler views. My view is not updated until I used notifyDataSetChanged. I asked for a similar type of issue, but this time I have Github Link. So please have a look and explain to me what I am doing wrong. Thanks
MainActivity.kt
package com.example.diffutilexample
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.diffutilexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var groupAdapter: GroupAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.fetchData()
binding.button.setOnClickListener {
viewModel.addData()
}
}
private fun setupViewModel() {
viewModel.groupListLiveData.observe(this) {
if (groupAdapter == null) {
groupAdapter = GroupAdapter()
binding.recyclerview.adapter = groupAdapter
}
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
binding.recyclerview.post {
groupAdapter?.notifyDataSetChanged()
}
}
}
}
ActivityViewModel.kt
package com.example.diffutilexample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var groupListLiveData: MutableLiveData<Boolean> = MutableLiveData()
var groupList: ArrayDeque<Group>? = null
set(value) {
field = value
groupListLiveData.postValue(true)
}
var value = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
groupList = groupByData(response.abc)
}
}
private fun groupByData(abc: List<Abc>?): ArrayDeque<Group> {
val result: ArrayDeque<Group> = groupList ?: ArrayDeque()
abc?.iterator()?.forEach { item ->
val key = GroupKey(item.qwe)
result.addFirst(Group(key, mutableListOf(item)))
}
return result
}
fun addData() {
groupList?.let { lastList ->
val qwe = Qwe("Vivek ${value++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val lastGroup = lastList[0]
lastGroup.list.add(item)
groupList = lastList
}
}
}
Please find the whole code in Github Link. I attached in above
I haven't debugged this, but if you remove your overuse of MutableLists and vars, and simplify your LiveData, you will likely eliminate your bug. At the very least, it will help you track down the problem.
MutableLists and DiffUtil do not play well together!
For example, Group's list should be a read-only List:
data class Group(
val key: GroupKey,
val list: List<Abc?> = emptyList()
)
It's convoluted to have a LiveData that only reports if some other property is usable. Then you're dealing with nullability all over the place here and in the observer, so it becomes hard to tell when some code is going to be skipped or not from a null-safe call. I would change your LiveData to directly publish a read-only List. You can avoid nullable Lists by using emptyList() to also simplify code.
You can avoid publicly showing your interior workings with the ArrayDeque as well. And you are lazy loading the ArrayDeque unnecessarily, which leads to having to deal with nullability unnecessarily.
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
private val _groupList = MutableLiveData<List<Group>>()
val groupList: LiveData<List<Group>> get() = _groupList
private val trackedGroups = ArrayDeque<Group>()
private var counter = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
addFetchedData(response.abc.orEmpty())
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
private fun addFetchedData(abcList: List<Abc>) {
for (item in abcList) {
val key = GroupKey(item.qwe)
trackedGroups.addFirst(Group(key, listOf(item)))
}
}
fun addData() {
if (trackedGroups.isEmpty())
return // Might want to create a default instead of doing nothing?
val qwe = Qwe("Vivek ${counter++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val group = trackedGroups[0]
trackedGroups[0] = group.copy(list = group.list + item)
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
In your Activity, since your GroupAdapter has no dependencies, you can instantiate it at the call site to avoid dealing with lazy loading it. And you can set it to the RecyclerView in onCreate() immediately.
Because of the changes in ViewModel, observing becomes very simple.
If you do something in setupViewModel() that updates a view immediately, you'll have a crash, so you should move it after calling setContentView().
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private val groupAdapter = GroupAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).apply {
setContentView(root)
recyclerview.adapter = groupAdapter
button.setOnClickListener {
viewModel.addData()
}
}
setupViewModel()
viewModel.fetchData()
}
private fun setupViewModel() {
viewModel.groupList.observe(this) {
groupAdapter.submitList(it)
}
}
}
Your DiffUtil.ItemCallback.areItemsTheSame in GroupAdapter is incorrect. You are only supposed to check if they represent the same item, not if their contents are the same, so it should not be comparing lists.
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem.key == newItem.key
}
And in GroupViewHolder, you are creating a new adapter for the inner RecyclerView every time it is rebound. That defeats the purpose of using RecyclerView at all. You should only create the adapter once.
I am predicting that the change in the nested list is going to look weird when the view is being recycled rather than just updated, because it will animate the change from what was in the view previously, which could be from a different item. So we should probably track the old item key and avoid the animation if the new key doesn't match. I think this can be done in the submitList() callback parameter to run after the list contents have been updated in the adapter by calling notifyDataSetChanged(), but I haven't tested it.
class GroupViewHolder(val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
companion object {
//...
}
private val adapter = NestedGroupAdapter().also {
binding.nestedRecyclerview.adapter = it
}
private var previousKey: GroupKey? = null
fun bindItem(item: Group?) {
val skipAnimation = item?.key != previousKey
previousKey = item?.key
adapter.submitList(item?.list.orEmpty()) {
if (skipAnimation) adapter.notifyDataSetChanged()
}
}
}
Side note: your adapters' bindView functions are confusingly named. I would just make those into secondary constructors and you can make the primary constructor private.
class GroupViewHolder private constructor(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
constructor(parent: ViewGroup) : this(
ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
//...
}
I'm not entirely sure, and I admit I haven't extensively studied your code, and this is not a solution, but this might point you in the right direction of how to solve it.
The thing about
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
Is that toMutableList() does indeed make a copy of the list. But each of the objects in the list are not copies. If you add things to an object in the original list, like you do in addData() it in fact is also already added to the copy that is in the adapter. That's why a new submitList doesn't recognize it as a change because it is actually the same as it was before the submitList.
As far as I understand, working with DiffUtil works best if the list you submit only contains objects that are immutable, so mistakes like this can't happen. I have ran into a similar problem before and the solution is also not straightforward. In fact, I don't entirely remember how I solved it back then, but hopefully this pushes you in the right direction.
Related
So I created MVVM app in kotlin to fetch movies from TMDB api, using injections and coroutines.
My problem is that I cannot copy the list of returned movies into a new list I created or reassign any variables inside the livedata observer from the MainActivity the values of variables stays the same as they were after exit the scope.
MainActivity class:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private val viewModel:MoviesViewModel by lazy {
ViewModelProvider(this)[MoviesViewModel::class.java]
}
private lateinit var list: MutableList<Movies>
private var number:Int=1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
getData()
list
number
}
private fun getData(){
list= mutableListOf()
viewModel.getData(page = "1").observe(this#MainActivity,Observer{ item ->
item?.let { res ->
when (res.status) {
Status.SUCCESS -> {
var tmpList = item.data?.body()?.results
list= tmpList!!.toMutableList()
number+=1
}
Status.ERROR -> {
res.message?.let { Log.e("Error", it) }
}}}
})}}
ViewModel class:
class MoviesViewModel #ViewModelInject constructor(var repository: MoviesRepository): ViewModel() {
fun getData(page:String)= liveData(Dispatchers.IO){
emit(Resource.loading(data = null))
try {
emit(Resource.success(data=repository.getMovies(api_key = Constants.API_KEY,
start_year=Constants.START_YEAR, end_year = Constants.END_YEAR,page = page)))
}catch (e:Exception){
emit(e.message?.let { Resource.error(message = it, data = null) })
}
}
}
As you can see I tried to change the value of number and load the list into my new list but outside the scope the values returned to be what they were before.
Very thankful for anyone who can assist.
Update:
So I tried to initialized all the items inside the success case and it worked I guess there is no other way to change the values outside the scope.
I have a MutableLiveData list in my repository as follows :
class AnswerRepository {
private var _answerList = mutableListOf<Answer>()
var answerList = MutableLiveData<MutableList<Answer>>()
fun addAnswerInList(answer: Answer) {
_answerList.add(answer)
answerList.value = _answerList
Log.e("AnswerRepository", "Answer List size : ${answerList.value?.size}")
Log.e("AnswerRepository", "_Answer List Size : ${_answerList.size}")
}
fun returnAnswerList(): MutableLiveData<MutableList<Answer>> {
return answerList
}
}
An item is added in 'answerList' (the MutableLiveData List) in a service as given below :
class FloatingWidgetService : Service(), View.OnClickListener{
private val answerRepository = AnswerRepository()
#SuppressLint("InflateParams")
override fun onCreate() {
super.onCreate()
//Getting the widget layout from xml using layout inflater
mFloatingView = LayoutInflater.from(this).inflate(R.layout.floating_widget, null)
initialiseViews()
setListeners()
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.next -> {
addItemInList()
}
}
private fun addItemInList(){
val answer = Answer(questionNumber, selectedOption, questionStatus)
answerRepository.addAnswerInList(answer)
}
Then this MutableLiveData List (answersList) is being observed in the fragment using viewmodel between repository and the fragment as follows :
ViewModel :
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
val answerList = answerRepository.returnAnswerList()
}
Fragment :
class SendAnswerToCloud : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentSendDataToCloudBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_send_data_to_cloud,
container,
false
)
binding.lifecycleOwner = this
viewModel.answerList.observe(viewLifecycleOwner, Observer {
Log.e("SendAnswerToCloud", "isChangeTrigerred")
val answer = viewModel.answerList.value?.last()
Log.e(
"SendAnswerToCloud",
"QuestionNumber : ${answer?.questionNumber}, SelectedOption : ${answer?.selectedOption}, QuestionStatus : ${answer?.questionStatus}"
)
})
return binding.root
}
}
The list is successfully updated in the repository when addAnswerInListis called in the service. However nothing happens in the fragment (as the logs don't appear in the logcat).
So, what am I doing wrong ? Any kind of help would be highly appreciated. Thanks in Advance!!
The problem in your implementation is that you instantiate two AnswerRepository objects instead of one. Thus, you get two var answerList = MutableLiveData<MutableList<Answer>>() instead of one. While your SendAnswerToCloud to cloud listening for changes on the first answerList your service edits the other answerList. That is the reason you do not see any changes.
Make sure you create only one AnswerRepository object.
In programming people use dependency injection and optionally in combination with singleton pattern. Sometimes you can get away using only singleton pattern, but this is a less flexible solution and not so easy to test.
Detailed reponse
So the problem occurs because you have first object instantiation in FloatingWidgetService class:
class FloatingWidgetService : Service(), View.OnClickListener{
private val answerRepository = AnswerRepository()
...
and the second instantiation in SendAnswerToCloudViewModel class:
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
...
}
This way you create two absolutely separate objects. Each one of them occupies different address in memory, and all of the objects you create inside AnswerRepository are also different between these two instances.
Imagine placing those declarations one after the other like this:
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
private val answerRepository_second = AnswerRepository()
...
}
If you later compare them by equals method or by == operator you will get result false because they are two different objects. Thus, if you set a new value to answerList of answerRepository subscribers of answerList stored in answerRepository_second will not receive any updates.
Solution
Add companion object and make the primary constructor private.
class AnswerRepository private constructor() {
companion object {
private var INSTANCE: AnswerRepository? = null
fun getInstance(): AnswerRepository {
if (INSTANCE == null) {
INSTANCE = AnswerRepository()
}
return INSTANCE!!
}
}
private var _answerList = mutableListOf<Answer>()
var answerList = MutableLiveData<MutableList<Answer>>()
fun addAnswerInList(answer: Answer) {
_answerList.add(answer)
answerList.value = _answerList
Log.e("AnswerRepository", "Answer List size : ${answerList.value?.size}")
Log.e("AnswerRepository", "_Answer List Size : ${_answerList.size}")
}
fun returnAnswerList(): MutableLiveData<MutableList<Answer>> {
return answerList
}
}
Now instead of writing declarations with constructor invocation:
private val answerRepository = AnswerRepository()
You will call getInstance() method to get AnswerRepository.
private val answerRepository = AnswerRepository.getInstance()
This pattern is called singleton. When you ensure that your program has only one instance of a specific class.
Strongly recommend you to complete Essentials and Kotlin maps here.
I'm fairly new to Kotlin/Android development, and am trying to figure out the best way to update data in a Room database. After following some tutorials, I currently have an architecture that looks like this:
Room Database with tables and DAOs -> Repository -> ViewModel -> Activity
So the activity has a ViewModel that calls the Repository, which in turn updates the database.
The ViewModel for the activity has a LiveData list of the object (there's also a factory to create the ViewModel, but that's just to allow the bookId to be passed in):
class ViewBookViewModel(application: Application, bookId: Int) : AndroidViewModel(application) {
private val repository: AppRepository
internal val flashCards: LiveData<List<FlashCard>>
init {
val flashCardDao = AppDatabase.getDatabase(application, viewModelScope).flashCardDao()
val bookDao = AppDatabase.getDatabase(application, viewModelScope).bookDao()
repository = AppRepository(flashCardDao, bookDao)
flashCards = flashCardDao.getByBookId(bookId)
}
fun insert(flashCard: FlashCard) = viewModelScope.launch(Dispatchers.IO){
repository.insert(flashCard)
}
fun setIsFavorited(cardUid: Long, favorited: Boolean) = viewModelScope.launch(Dispatchers.IO) {
repository.setIsFavorited(cardUid, favorited)
}
}
//The actual query that gets called eventually
#Query("UPDATE flashcard SET is_favorited = :favorited WHERE uid LIKE :cardUid")
fun setFavorited(cardUid: Long, favorited: Boolean)
And the Activity sets up the viewModel and also creates an observer on the
class ViewBookActivity : AppCompatActivity() {
private lateinit var flashCards: LiveData<List<FlashCard>>
private var layoutManager: RecyclerView.LayoutManager? = null
private lateinit var viewModel: ViewBookViewModel
private var bookId: Int = 0
private lateinit var bookTitle: String
override fun onCreate(savedInstanceState: Bundle?) {
...
bookId = intent.extras["bookId"] as Int
bookTitle = intent.extras["bookTitle"].toString()
layoutManager = LinearLayoutManager(this)
flashCardRecyclerView.layoutManager = layoutManager
viewModel = ViewModelProviders.of(this, ViewBookViewModelFactory(application, bookId as Int)).get(ViewBookViewModel::class.java)
flashCards = viewModel.flashCards
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
})
}
}
Finally, I have a custom RecyclerAdapter, which is where I'm running into trouble. I have it set up so that when the user taps the "favorite" button on the Flash Card, it updates the database. However, this also causes the Activity to "refresh", scrolling to the top. I assume this is because it is observing LiveData, and that data is being changed.
custom RecylcerAdapter with ViewHolder code (stripped not-relevant code):
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val viewModel: ViewBookViewModel) : RecyclerView.Adapter<FlashCardRecyclerAdapter.FlashCardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlashCardViewHolder {
val v: View = LayoutInflater
.from(parent.context)
.inflate(R.layout.flash_card, parent, false)
return FlashCardViewHolder(v)
}
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
override fun getItemCount(): Int {
return flashCards.size
}
inner class FlashCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
var mFavorited: Button
var frontShowing: Boolean
var isFavorited: Boolean = false
var uid: Long = 0
init {
mFavorited = itemView.findViewById(R.id.favoriteButton)
mFavorited.setOnClickListener { _ ->
isFavorited = !isFavorited
viewModel.setIsFavorited(uid, isFavorited) // Here is the database call
modifyFavoriteButtonImage(isFavorited)
}
}
fun modifyFavoriteButtonImage(isFavorited: Boolean){
// Code removed, just updates the image to be a filled/empty star based on favorited status
}
}
I feel like I am probably doing something wrong, as passing the ViewModel into the recylcer adapter in order to update the DB does not seem correct. Is there a pattern I should be using for this sort of situation, or should I change the code to not be using LiveData? Any help would be greatly appreciated.
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
}
you should not be making a new adapter instance here, instead, assign the values you get from the live data to the existing adapter (adapter.flashCards = flashCards, LiveData value) and call adapter.notifyDataSetChanged, this will tell your adapter that new data came in and it needs to update.
you should not be passing your ViewModel to your adapter (or anything).
you can do something like this instead:
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val callback:(FlashCard) -> Unit)
then, where you declare your adapter, you do this :
val adapter = FlashCardRecyclerAdapter(...) {
viewModel.update(it)
}
and then :
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.itemView.setOnClickListener {
callback.invoke(card)
}
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
In your repository method, I am not sure what you are doing there but rather than passing in a livedata instance, you should pass in the underlying data of the livedata instance. That way, the observer in the main activity doesn't get triggered everytime you call setIsFavorited(). If you do want to trigger the observer, then you can just call postValue() on the livedata instance. As for the adapter question, I do not know the best practices but I usually create a listener interface so I don't have to pass around my viewmodels everywhere. All of my viewmodels are contained within my fragments and never goes anywhere else. Let me know if this answers your questions.
Also, if you are using viewmodels with recyclerview, consider using list adapters. They are made to work seamlessly with viewmodels. https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter
It makes it much simpler to use viewmodels with recyclerview.
Activity receiving intent
class AddNoteActivity : AppCompatActivity() {
private lateinit var addViewModel: NoteViewModel
private lateinit var titleEditText: TextInputEditText
private lateinit var contentEditText: TextInputEditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_note_activty)
setSupportActionBar(toolbar)
addViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
titleEditText = findViewById(R.id.itemTitle)
contentEditText = findViewById(R.id.itemNote)
val extra = intent.extras
if (extra != null) {
val uuid = extra.getLong("UUID")
val note: Notes? = addViewModel.getNote(uuid)
titleEditText.setText(note!!.title)
contentEditText.setText(note.note)
}
}
}
NoteViewModel class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private var note: Notes = Notes(0, "", "test title", "test ontent")
}
fun getNote(uuid: Long?): Notes {
val job = async(CommonPool) {
getNoteAsyncTask(notesDatabase).execute(uuid)
}
runBlocking { job.await() }
return note
}
class getNoteAsyncTask(database: NotesDatabase) : AsyncTask<Long, Unit, Unit>() {
private val db: NotesDatabase = database
override fun doInBackground(vararg params: Long?) {
note = db.notesDataDao().getNote(params[0])
}
}
}
If I pass an intent to get a Note object from the database with a uuid and set that received data in titleEditText and contentEditText, the data set in the Note was from previous intent invoked when we clicked on the Note item in RecyclerView. On clicking the Note item for the first time, I get the default value which I have set "test title" and "test content".
Aforementioned is the behavior most of the time. Sometimes the data set in titleEditText and contentEditText is of the correct Note object.
Can someone please tell me what I have done wrong? How can I correct my apps behavior?
Unfortunately, there is a big mistake in how you use a view model to provide a data to your view(AddNoteActivity).
Basically, your view never has a chance to wait for the data to be fetched as it always receives a default value. This happens because the AsyncTask runs on its own thread pool so the coroutine completes immediately and returns a default value.
You should consider using LiveData to post a new object to your view and refactor your view model.
So, you need to make a query to the database synchronous and observe changes to a note rather than have a getter for it. Of course, in a real life scenario it might be a good idea to have different kind of states to be able to show a spinner while a user is waiting. But this is another big question. So to keep things simple consider changing your view model to something like that:
class NoteViewModel(private val database: NotesDatabase) : ViewModel { // you do not need an application class here
private val _notes = MutableLiveData<Notes>()
val notes: LiveData<Notes> = _notes
fun loadNotes(uuid: Long) {
launch(CommonPool) {
val notes = database.notesDataDao().getNote(uuid)
_notes.setValue(notes)
}
}
}
Then, you can observe changes to the note field in your activity.
class AddNoteActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val noteViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
noteViewModel.notes.observe(this, Observer {
title.text = it.title
content.text = it.note
})
}
}
Also you need to use a ViewModelProvider.Factory to create your view model and properly inject dependencies into it. Try to avoid having a context there as it makes it much harder to test.
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.