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.
Related
I'm trying to fetch multiple documents from my Firestore collection so I can populate my RecyclerView. However, I'm getting a mismatch error when I try to hook my categories ArrayList to the QuerySnapshot, it says it's looking for kotlin.collections.ArrayList<Category> but it found Category?. What can I do to make my RecyclerView populate my category collection in Firestore? Do I need to rewrite my val categories = ArrayList<Category>()? Thank you!
Category Collection
Category.kt
data class Category(var category: String?, val categoryImage: String?) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(category)
parcel.writeString(categoryImage)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Category> {
override fun createFromParcel(parcel: Parcel): Category {
return Category(parcel)
}
override fun newArray(size: Int): Array<Category?> {
return arrayOfNulls(size)
}
}
}
CategoryAdapter.kt
class CategoryAdapter(val category: ArrayList<Category>) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
var selectedCategory = Category("", "")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindCategory(category[position])
holder.itemView.setOnClickListener { v ->
val context: Context = v.context
val intent = Intent(context, CategoryServiceActivity::class.java)
selectedCategory.category = category[position].category
intent.putExtra("category", selectedCategory)
context.startActivity(intent)
}
}
override fun getItemCount(): Int {
return category.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.categoryrecyclyerview, parent, false)
return ViewHolder(view)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val categoryName = itemView.findViewById<TextView>(R.id.categoryJobNameTextView)
val categoryImage = itemView.findViewById<ImageView>(R.id.categoryImageView)
fun bindCategory(category: Category) {
categoryName?.text = category.category
Picasso.get().load(category.categoryImage).into(categoryImage)
}
}
}
HomeFragment.kt
val categories = ArrayList<Category>()
val categoriesDatabaseRef = FirebaseFirestore.getInstance().collection(REF_JOB_CATEGORIES)
categoriesDatabaseRef.orderBy("category").get().addOnSuccessListener(object: OnSuccessListener<QuerySnapshot> {
override fun onSuccess(p0: QuerySnapshot?) {
if (p0 != null) {
for (querySnapshot in p0.documents) {
categories = querySnapshot.toObject(Category::class.java)
}
}
}
})
As I see in your code, the categories object is an ArrayList. So when you're using the following line of code:
categories = querySnapshot.toObject(Category::class.java)
It means that you're trying to convert the querySnapshot object, which is actually a DocumentSnapshot object, into an object of type Category, which works perfectly fine. However, you cannot assign that value to the categories object because between the ArrayList and Category classes, there is no inheritance relationship, hence that error.
So there are two ways in which you can solve this. The first solution would be to add an object of type Category, at each iteration of the for loop to the list:
categoriesDatabaseRef.orderBy("category").get().addOnSuccessListener(object: OnSuccessListener<QuerySnapshot> {
override fun onSuccess(p0: QuerySnapshot?) {
if (p0 != null) {
for (querySnapshot in p0.documents) {
val category = querySnapshot.toObject(Category::class.java)
categories.add(category) //Add the object to the list.
}
}
}
})
The second solution, which is even simpler in my opinion, would be to convert the querySnapshot directly into a list, by removing the for loop like this:
categoriesDatabaseRef.orderBy("category").get().addOnSuccessListener(object: OnSuccessListener<QuerySnapshot> {
override fun onSuccess(p0: QuerySnapshot?) {
if (p0 != null) {
categories = p0.toObjects(Category::class.java)
}
}
})
Please see that I have used toObjects(Class clazz) method which:
Returns the contents of the documents in the QuerySnapshot, converted to the provided class, as a list.
So it's toObjects, see the s? And not toObject.
Besides that, don't forget that Firebase API is asynchronous. So you cannot simply use the value of categories outside the onSuccess() method. If you're new to asynchronous programming, I recommend you read the following resource:
How to read data from Cloud Firestore using get()?
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 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.
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.