Realm query update not reflected in RealmRecyclerViewAdapter - android

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.

Related

Recycler view not updating automatically with live 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.

Paging library: Jumping list items + always same data at the end

I am trying to implement an infinite list with the Paging library, MVVM and LiveData.
In my View (in my case my fragment) I ask for data from the ViewModel and observe the changes:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getItems("someSearchQuery")
viewModel.pagedItems.observe(this, Observer<PagedList<Item>> {
// ItemPagedRecyclerAdapter
// EDIT --> Found this in the official Google example
// Workaround for an issue where RecyclerView incorrectly uses the loading / spinner
// item added to the end of the list as an anchor during initial load.
val layoutManager = (recycler.layoutManager as LinearLayoutManager)
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
if (position != RecyclerView.NO_POSITION) {
recycler.scrollToPosition(position)
}
})
}
In the ViewModel I fetch my data like this:
private val queryLiveData = MutableLiveData<String>()
private val itemResult: LiveData<LiveData<PagedList<Item>>> = Transformations.map(queryLiveData) { query ->
itemRepository.fetchItems(query)
}
val pagedItems: LiveData<PagedList<Item>> = Transformations.switchMap(itemResult) { it }
private fun getItems(queryString: String) {
queryLiveData.postValue(queryString)
}
In the repository I fetch for the data with:
fun fetchItems(query: String): LiveData<PagedList<Item>> {
val boundaryCallback = ItemBoundaryCallback(query, this.accessToken!!, remoteDataSource, localDataSource)
val dataSourceFactory = localDataSource.fetch(query)
return dataSourceFactory.toLiveData(
pageSize = Constants.PAGE_SIZE_ITEM_FETCH,
boundaryCallback = boundaryCallback)
}
As you might have already noticed, I used the Codelabs from Google as an example, but sadly I could not manage to make it work correctly.
class ItemBoundaryCallback(
private val query: String,
private val accessToken: AccessToken,
private val remoteDataSource: ItemRemoteDataSource,
private val localDataSource: Item LocalDataSource
) : PagedList.BoundaryCallback<Item>() {
private val executor = Executors.newSingleThreadExecutor()
private val helper = PagingRequestHelper(executor)
// keep the last requested page. When the request is successful, increment the page number.
private var lastRequestedPage = 0
private fun requestAndSaveData(query: String, helperCallback: PagingRequestHelper.Request.Callback) {
val searchData = SomeSearchData()
remoteDataSource.fetch Items(searchData, accessToken, lastRequestedPage * Constants.PAGE_SIZE_ITEMS_FETCH, { items ->
executor.execute {
localDataSource.insert(items) {
lastRequestedPage++
helperCallback.recordSuccess()
}
}
}, { error ->
helperCallback.recordFailure(Throwable(error))
})
}
#MainThread
override fun onZeroItemsLoaded() {
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
requestAndSaveData(query, it)
}
}
#MainThread
override fun onItemAtEndLoaded(itemAtEnd: Item) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
requestAndSaveData(query, it)
}
}
My adapter for the list data:
class ItemPagedRecyclerAdapter : PagedListAdapter<Item, RecyclerView.ViewHolder>(ITEM_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ItemViewHolder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
(holder as ItemViewHolder).bind(item, position)
}
}
companion object {
private val ITEM_COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
olItem.id == newItem.id
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =
oldItem == newItem
}
}
}
My problem right now is: The data is fetched and saved locally and is even displayed correctly in my list. But the data seems to be "looping", so there is always the same data showing despite there are different objects in the database (I checked with Stetho, about several hundred). Curiously the last item in the list is also always the same and sometimes there are items reloading while scrolling. Another problem is that it stops reloading at some point (sometimes 200, sometimes 300 data items).
I thought it might be because my ITEM_COMPARATOR was checking wrongly and returning the wrong boolean, so I set both to return true just to test, but this changed nothing.
I was also thinking of adding a config to the LivePagedListBuilder, but this also changed nothing. So I am a little bit stuck. I also looked into some examples doing it with a PageKeyedDataSource etc., but Google's example is working without it, so I want to know why my example is not working.
https://codelabs.developers.google.com/codelabs/android-paging/index.html?index=..%2F..index#5
Edit:
Google does have another example in their blueprints. I added it to the code. https://github.com/android/architecture-components-samples/blob/master/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt.
Now it is loading correctly, but when the loading happens, some items in the list still flip.
Edit 2:
I edited the BoundaryCallback, still not working (now provided the PagingRequestHelper suggested by Google).
Edit 3:
I tried it just with the remote part and it works perfectly. There seems to be a problem with Room/the datasource that room provides.
Ok, just to complete this issue, I found the solution.
To make this work, you must have a consistent list-order from your backend/api-data. The flipping was caused by the data that was constantly sent in another order than before and therefore made some items in the list "flip" around.
So you have to save an additional field to your data (an index-like field) to order your data accordingly. The fetch from the local database in the DAO is then done with a ORDER BY statement. I hope I can maybe help someone who forgot the same as I did:
#Query("SELECT * FROM items ORDER BY indexFromBackend")
abstract fun fetchItems(): DataSource.Factory<Int, Items>
You have to override PageKeyedDataSource to implement paging logic with Paging library. Check out this link

How to update Room database in Kotlin without activity refreshing?

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.

Firebase Database notify adapter after retriving data?

I am trying to access data once it is completely retrieved from database? initially I apply adapter to fragment. With in the Adapter I tried to retrieve data from firebase database. So here give problem it send the null arraylist. It should send back the arraylist when complete data is retrieved?
Adapter code :
class FirebaseAdapter(context: Context): RecyclerView.Adapter<FirebaseAdapter.Holder>() {
var dataList: ArrayList<DatabaseOperations.ImageInfo> = arrayListOf()
var context: Context? = null
init {
if (context == null)
this.context = context
dataList= DatabaseOperations().retriveInfo(context!!)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
var itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.imagelist_row,parent)
var viewHolder: FirebaseAdapter.Holder = FirebaseAdapter.Holder(itemView)
return viewHolder
}
override fun getItemCount(): Int {
Log.e("itemCoutn",dataList.size.toString()) // it give output 0
return dataList.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
try {
//----------------get bitmap from image url-----------------
var downloadUri: String = dataList.get(position).downloadUri
Log.e("fire adapter",downloadUri.toString())
//------------------Assign Data to item here-----------------
holder.image_name.text = dataList.get(position).imageName
Glide.with(this!!.context!!)
.load(downloadUri)
.into(holder.row_image)
}
catch(e: Exception){
Log.e("Firebase Adapter","Error "+e.toString())
}
}
class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) {
val row_image: ImageView
val image_name: TextView
init {
row_image = itemView!!.findViewById<ImageView>(R.id.row_image)
image_name = itemView!!.findViewById<TextView>(R.id.image_name)
}
}
}
Information retrieve code :
fun retriveInfo( context: Context): ArrayList<ImageInfo>{
var data = ArrayList<ImageInfo>()
if (mDatabaseRefrence == null)
mDatabaseRefrence = FirebaseDatabase.getInstance().getReference(getUid())
val menuListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
var dataSnap: DataSnapshot? = null
var it: Iterable<DataSnapshot> = dataSnapshot.children
it.forEach { dataSnapshot ->
data.add(ImageInfo(
dataSnapshot!!.child("imageName").toString(),
dataSnapshot!!.child("imageInfo").toString(),
dataSnapshot!!.child("downloadUri").toString()
))
}
FirebaseAdapter(context).notifyDataSetChanged()
Log.e("db size 0",data.size.toString())
}
override fun onCancelled(databaseError: DatabaseError) {
println("loadPost:onCancelled ${databaseError.toException()}")
}
}
mDatabaseRefrence!!.addValueEventListener(menuListener)
Log.e("db size",data.size.toString())
return data
}
You cannot return something now that hasn't been loaded yet. With other words, you cannot simply return the data list as a result of a function because the list it will always be empty due the asynchronous behaviour of this function. This means that by the time you are trying to return that result, the data hasn't finished loading yet from the database and that's why is not accessible.
Basically, you're trying to return a value synchronously from an API that's asynchronous. That's not a good idea. You should handle the APIs asynchronously as intended.
A quick solve for this problem would be to use the data list only inside the callback (inside the onDataChange() method). If you want to use it outside, I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.

How to update an element in ArrayList without knowing index in Android with Firebase Realtime Database?

I have a simple RecyclerView with CardView as list items. Each item of the list retrieved from Firebase Realtime databse and stored to noteList. Firebase sdk provides callbacks for onChildAdded, onChildChanged and few others. Now I have designed the Adapter for recycler view to take in an ArrayList. I use the callbacks to add or remove items to the noteList whenever the data in Realtime database changes. The onChildChanged method is called whenever a value of particular item changes. I want to reflect this change in noteList and notify the adapter of the same.
Ofcourse, we can naively search for each element in the List matching the changed element's key and update it. But can this be done in a better/efficient way?
class NoteListAdapter(var data: MutableList<Note>, val onItemClick: (Note) -> Unit) : RecyclerView.Adapter<NoteListAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
holder!!.bindNoteItem(data[position])
}
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent?.context)
.inflate(R.layout.card_note, parent, false)
return ViewHolder(v, onItemClick)
}
class ViewHolder(v: View, val onItemClick: (Note) -> Unit): RecyclerView.ViewHolder(v) {
fun bindNoteItem(note: Note) {
...
}
}
}
HomeActivity.kt
class HomeActivity : AppCompatActivity() {
lateinit var mDb: DatabaseReference
val noteList: MutableList<Note> = ArrayList()
lateinit var adapter: NoteListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
addNoteFAB.setOnClickListener { addNote() }
mDb = FirebaseDatabase.getInstance().reference
adapter = NoteListAdapter(noteList, this::editNote)
noteListRecycler.layoutManager = LinearLayoutManager(this)
noteListRecycler.adapter = adapter
loadNotes()
}
fun addNote() {
val intent = Intent(this, EditNoteActivity::class.java)
intent.putExtra(EditNoteActivity.EXTRA_NOTE, Note())
startActivity(intent)
}
fun loadNotes() {
val path = "users/" + FirebaseAuth.getInstance().currentUser?.uid + "/notes"
mDb.child(path ).addChildListener({
// onChildAdded
dataSnapshot, prevChildName ->
val note = dataSnapshot?.getValue(Note::class.java)!!
note.id = dataSnapshot.key
noteList += note
adapter.notifyDataSetChanged()
}, {
// onChildRemoved
dataSnapshot ->
noteList.remove(dataSnapshot?.getValue(Note::class.java))
adapter.notifyDataSetChanged()
}, {
// onChildChanged, TODO: update the change in elements value in noteList
dataSnapshot, s ->
})
}
fun editNote(note: Note) {
val i = Intent(this, EditNoteActivity::class.java)
i.putExtra(EditNoteActivity.EXTRA_NOTE, note)
startActivity(i)
}
}
PS: addChildListener is an extension function. (check comments in code for the name of callbacks)
Since dataSnapshot can only tell us the key and value that was changed (as far as I know from the docs), how will I update noteList without knowing the index of the changed element? Or is there really a way to get the index?
I would by far recommend that you use FirebaseUI which is built exactly for your use case.
However, if you'd really like to go solo, then no, the only way to get the index of an updated child is by finding it in your existing data. Here's an example from FirebaseUI's internals:
private int getIndexForKey(String key) {
int index = 0;
for (DataSnapshot snapshot : mSnapshots) {
if (snapshot.getKey().equals(key)) {
return index;
} else {
index++;
}
}
throw new IllegalArgumentException("Key not found");
}
Here's how FUI uses getIndexForKey(String).
Iterating through the list works just fine in terms of performance as long as you aren't downloading ridiculously large lists. Otherwise, you can use a Map<String, Integer> and see the attempt for FUI here.
PS: your onChildAdded code is wrong for inserts (items will always be added at the end even if they are in the middle). See how FUI does this.

Categories

Resources