← Back to Blog

Paginating Firebase Realtime Database in a RecyclerView within a NestedScrollView

Anonymous
7 min read

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

0 Comments

No comments yet. Be the first to share your thoughts!