Photo by Danial Igdery on Unsplash
If you’re using Firebase Realtime Database to store and retrieve data for your Android app, you may have encountered the challenge of paginating the data within a RecyclerView. This can be especially tricky when the RecyclerView is nested within a NestedScrollView, as the scrolling behavior of the scroll view can interfere with the recycler view’s scroll behavior, making it difficult to paginate data.
Paginating helps you load small chunks of data at a time. Loading partial data on demand reduces the usage of network bandwidth and system resources. When using the Cloud Firestore database, pagination can easily be achieved using query cursors. With query cursors, you can split data returned by a query into batches according to the parameters you define in your query. However, when using the Realtime Database, there is no official API for implementing pagination at the moment.
In this blog post, we’ll explore some effective strategies for paginating Firebase Realtime Database data in a RecyclerView within a NestedScrollView. We’ll discuss the benefits of using this approach and walk through the steps for implementing it in your own app. Whether you’re a beginner or an experienced developer, this post will provide valuable insights and techniques for paginating your Firebase Realtime Database data in a smooth and seamless way. So let’s get started!
Getting Started:
This guide uses the architecture components so you need to be familiar with View Binding, View Models, and Live Data. I also assume you already know how to load data in a recycler view using an adapter.
BooksModel.kt
data class ModelBook(var bId: String,
var title: String,
var description: String,
var authorId: String,
var authorName: String,
var image: String,
var book: String,
var category: String,
var datePublished: String,
var dateUploaded: String,
var best: String)
PagingAdapter.kt
class PagingAdapter(var context: Context): RecyclerView.Adapter<PagingAdapter.MyViewHolder>() {
private var booksList = ArrayList<ModelBook>()
fun addBooks(newBooks: ArrayList<ModelBook>) {
val listSize: Int = booksList.size
booksList.addAll(newBooks)
notifyItemRangeChanged(listSize, newBooks.size)
}
class MyViewHolder(val binding: BookCardItemBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ModelBook) {
binding.bookTitleTv.text = item.title
binding.authorTv.text = "By: " + item.authorName
binding.descrTv.text = item.description
binding.categoryChip.text = item.category
}
Glide.with(binding.bookIv.context).load(item.image).fallback(R.drawable.books)
.into(binding.bookIv)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return MyViewHolder(binding = BookCardItemBinding.inflate(layoutInflater, parent, false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = booksList[position]
holder.bind(item)
}
override fun getItemCount(): Int = booksList.size
}
BooksFragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.homePage.BooksFragment">
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
....
</androidx.cardview.widget.CardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
BooksFragment.kt
class BooksFragment : Fragment() {
private lateinit var binding: FragmentBooksBinding
private val adapter: PagingAdapter by lazy { PagingAdapter(requireContext()) }
private val viewModel: SharedViewModel by activityViewModels()
private var bookKey = ""
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
initViews()
fetchBooks()
welcomeUser()
return binding.root
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getBooksList(bookKey)
viewModel.currentUser()
}
private fun initViews() {
binding.nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, _, scrollY, _, _ ->
if (scrollY == (v.getChildAt(0).measuredHeight - v.measuredHeight)) viewModel.getBooksList(bookKey)
})
}
private fun fetchBooks() {
val keyObserver = Observer<String>{key -> bookKey = key }
viewModel.key.observe(requireActivity(), keyObserver)
val listObserver = Observer<List<ModelBook>?> { list ->
adapter.addBooks(list as ArrayList<ModelBook>)
binding.recyclerView.adapter = adapter
adapter.notifyDataSetChanged()
}
viewModel.bookList.observe(requireActivity(), listObserver)
}
}
SharedViewModel.kt
class SharedViewModel(private val savedHandle: SavedStateHandle) : ViewModel() {
private val _bookList = MutableLiveData<List<ModelBook>?>()
val bookList: LiveData<List<ModelBook>?> get() = _bookList
private val _key = MutableLiveData<String>()
val key: LiveData<String> get() = _key
fun getBooksList(lastKey: String){
_loadingStatus.value = LoadingStatus.LOADING
val query: Query = if (TextUtils.isEmpty(lastKey)) FirebaseDatabase.getInstance()
.getReference(BOOKS)
.orderByKey()
.limitToFirst(ITEM_COUNT) else FirebaseDatabase.getInstance()
.getReference(BOOKS)
.orderByKey()
.startAfter(lastKey)
.limitToFirst(ITEM_COUNT)
query.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.hasChildren()) {
val newBooks = ArrayList<ModelBook>()
for (userSnapshot in snapshot.children) {
userSnapshot.getValue(ModelBook::class.java)
?.let { newBooks.add(it) }
}
_key.value = newBooks[newBooks.size - 1].bId!!
_bookList.value = newBooks
}
}
override fun onCancelled(error: DatabaseError) {
}
})
}
}
Output
Thank you!
If you need any help, don’t hesitate to hit me up on twitter, Github