Hey people! This article helps you in understanding to implement RecyclerView using data available in Cloud Firestore in No-SQL form of collections and documents.

Cloud Firestore belongs to Firebase, you can read more about firestore before you begin. Learn it’s key capabilities and how does it work from official documents. Let’s begin with outline and implementation also find helpful resources at the end of blog.


1. Create new android studio project

Start with new android studio project choosing Empty Activity.
Give project and package name and select language Kotlin and finish.
You can see the source code from my Github project.


2. Setup project in firebase console.

If you have already done setting up your project in firebase console skip this step.
Alternatively you can follow all instructions from firestore documentations.

  • Create a new firebase project from firebase console for app and follow instructions to complete setup.
  • Create a database after navigating to cloud firestore. I will show how I have structured my database after adding the dependencies.

3. Add project dependencies.

Recheck if you have included these dependencies and plugins without missing them.

Add this google services plugin to app directory. [ build.gradle(:app) ]

apply plugin: 'com.google.gms.google-services'

Add this google services plugin to module directory. [ build.gradle(MyProject) ]

classpath 'com.google.gms:google-services:4.3.4'

Finally add Firestore , RecyclerView dependencies. Also make sure to add ConstraintLayout and Material dependencies.

implementation 'com.google.firebase:firebase-firestore:22.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06'

4. Structure collections and documents.

Collection Name = sports
First Document = sport_1
Document Fields :
1. id : 1 (Int)
2. originated : “1823” (String)
3. title : “Rugby” (String)


Image preview on firestore cloud database
Firestore firebase preview

You can give your own naming to the documents or it will generate automatically one for you. Deciding on how you want to structure your data, you may want to name them in certain order. This helps you filter documents correctly when performing query operations.


5. Create FirestoreAdapter for listening to events.

By implementing EventListener<QuerySnapshot> to our class we can listen to the changes occurring to the documents.

// New document was added to the set of documents matching the query.
DocumentChange.Type.ADDED

// New document within the query was modified.
DocumentChange.Type.MODIFIED

// New document within the query was removed, deleted,no longer matches the query.
DocumentChange.Type.REMOVED

Now let’s create a abstract adapter class FirestoreAdapter of type RecyclerView.ViewHolder which extends to RecylerView.Adapter<VH>( ) and EventListener<QuerySnapshot> also takes parameter Query like below.

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(
    private val query: Query
) : RecyclerView.Adapter<VH>(), EventListener<QuerySnapshot> {

}

Now let’s implement all methods to adapter which listens to the changes using onEvent( ). Call the override function.

override fun onEvent(
        documentSnapshots: QuerySnapshot?,
        exception: FirebaseFirestoreException?,
) {
    // Handle errors
    if (exception != null) {
        Log.e("onEvent:error", exception.toString())
        return
    }

    // Dispatch the event
    for (change in documentSnapshots!!.documentChanges) {
        // Snapshot of the changed document
        when (change.type) {
            DocumentChange.Type.ADDED -> onDocumentAdded(change)
            DocumentChange.Type.MODIFIED -> onDocumentModified(change)
            DocumentChange.Type.REMOVED -> onDocumentRemoved(change)
        }
    }
}

Create a snapshot variable for ArrayList of type DocumentSnapshot, this variable has all document snapshots which we will work with.

private val snapshots = ArrayList<DocumentSnapshot>()

Now you need to create methods for Document Changes. All those changes will be made to previously created snapshot variable. Each time changes are made we use their index for doing it correctly, and lastly we notify those changes with index.

protected open fun onDocumentAdded(change: DocumentChange) {
    snapshots.add(change.newIndex, change.document)
    notifyItemInserted(change.newIndex)
}

protected open fun onDocumentModified(change: DocumentChange) {
    if (change.oldIndex == change.newIndex) {
        // Item changed but remained in same position
        snapshots[change.oldIndex] = change.document
        notifyItemChanged(change.oldIndex)
    } else {
        // Item changed and changed position
        snapshots.removeAt(change.oldIndex)
        snapshots.add(change.newIndex, change.document)
        notifyItemMoved(change.oldIndex, change.newIndex)
    }
}

protected open fun onDocumentRemoved(change: DocumentChange) {
    snapshots.removeAt(change.oldIndex)
    notifyItemRemoved(change.oldIndex)
}

Now we need functions which listens to those document changes, we call this functions in our activity and fragments whenever we need to get data.
Create a variable with name registration of type ListenerRegistration which listens to the query using SnapshotListener.

private var registration: ListenerRegistration? = null

open fun startListening() {
    if (registration == null) {
        registration = query.addSnapshotListener(this)
    }
}

open fun stopListening() {
    if (registration != null) {
        registration!!.remove()
        registration = null
    }

    snapshots.clear()
    notifyDataSetChanged()
}

Finally return the item count getItemCount( ) and document snapshot getSnapshot( ) in the adapter class.
This is how our abstract class looks after making these changes.

import android.util.Log
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.firestore.*
import com.google.firebase.firestore.EventListener
import java.util.*

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(
    private val query: Query
) : RecyclerView.Adapter<VH>(), EventListener<QuerySnapshot> {

    private var registration: ListenerRegistration? = null
    private val snapshots = ArrayList<DocumentSnapshot>()

    open fun startListening() {
        if (registration == null) {
            registration = query.addSnapshotListener(this)
        }
    }

    open fun stopListening() {
        if (registration != null) {
            registration!!.remove()
            registration = null
        }

        snapshots.clear()
        notifyDataSetChanged()
    }

    override fun onEvent(
        documentSnapshots: QuerySnapshot?,
        exception: FirebaseFirestoreException?
    ) {
        if (exception != null) {
            Log.e("onEvent:error", exception.toString())
            return
        }

        for (change in documentSnapshots!!.documentChanges) {
            when (change.type) {
                DocumentChange.Type.ADDED -> onDocumentAdded(change)
                DocumentChange.Type.MODIFIED -> onDocumentModified(change)
                DocumentChange.Type.REMOVED -> onDocumentRemoved(change)
            }
        }
    }

    protected open fun onDocumentAdded(change: DocumentChange) {
        snapshots.add(change.newIndex, change.document)
        notifyItemInserted(change.newIndex)
    }

    protected open fun onDocumentModified(change: DocumentChange) {
        if (change.oldIndex == change.newIndex) {
            snapshots[change.oldIndex] = change.document
            notifyItemChanged(change.oldIndex)
        } else {
            snapshots.removeAt(change.oldIndex)
            snapshots.add(change.newIndex, change.document)
            notifyItemMoved(change.oldIndex, change.newIndex)
        }
    }

    protected open fun onDocumentRemoved(change: DocumentChange) {
        snapshots.removeAt(change.oldIndex)
        notifyItemRemoved(change.oldIndex)
    }

    override fun getItemCount(): Int {
        return snapshots.size
    }

    protected open fun getSnapshot(index: Int): DocumentSnapshot? {
        return snapshots[index]
    }
}

6. Make a query for collection and documents.

Our entire data is in collection “sports” which contains list of documents. Each document has fields which are properties for our each sport such as rugby.

To read and get that data we need to call them by building a Query.
These queries can be constructed with filters and order which helps structure our data comes in form of documents.

Let’s create a query for our collection “sports”.
You need to call collection after creating a firestore instance.

val query: Query = FirebaseFirestore.getInstance().collection("sports")

Once the call is successful, it returns all the documents inside that collection.
Using DocumentSnapshot we will be able to get specific properties or fields for each document, this part of execution is done from adapter class.


7. Create model class and RecyclerView adapter.

Each field in document maps to our model class properties.
Example: Document field title: “Rugby” maps to val title: String

import com.google.firebase.firestore.IgnoreExtraProperties

@IgnoreExtraProperties
class Sports(
        val title: String? = null,
        val originated: String? = null,
)

The annotation IgnoreExtraProperties will helps us by not mapping the fields to our model class properties which are not available.

Create layout file for showing each item in list.

item_sport.xml:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 
    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="wrap_content"
    android:layout_marginHorizontal="16dp"
    android:layout_marginTop="6dp"
    app:cardBackgroundColor="?attr/colorPrimaryVariant"
    app:cardUseCompatPadding="true">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingHorizontal="16dp"
        android:paddingVertical="12dp">

        <TextView
            android:id="@+id/item_sport_title_text_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textAppearance="?attr/textAppearanceHeadline6"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Rugby" />

        <TextView
            android:id="@+id/item_sport_originated_text_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="4dp"
            android:textAppearance="?attr/textAppearanceSubtitle1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@id/item_sport_title_text_view"
            tools:text="1823" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>

Now create RecyclerView adapter class which takes Query parameter and extends from FirestoreAdapter which gives us access to getting DocumentSnapshot.

SportsAdapter.kt:

import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.Query

class SportsAdapter(query: Query) : FirestoreAdapter<SportsAdapter.SportsViewHolder>(query) {

    class SportsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private val title: TextView = itemView.findViewById(R.id.item_sport_title_text_view)
        private val originated: TextView = itemView.findViewById(R.id.item_sport_originated_text_view)

        fun bind(snapshot: DocumentSnapshot) {
            val sports: Sports? = snapshot.toObject(Sports::class.java)
            title.text = sports?.title
            originated.text = sports?.originated
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SportsViewHolder {
        return SportsViewHolder(LayoutInflater.from(parent.context).inflate(
                R.layout.item_sport, parent, false)
        )
    }

    override fun onBindViewHolder(holder: SportsViewHolder, position: Int) {
        getSnapshot(position)?.let { snapshot -> holder.bind(snapshot) }
    }
}

You don’t have to override getItemCount( ) here because our abstract class FirestoreAdapter.kt already has one.
Now let’s attach our query and adapter to our activity class.


8. Listen to data changes using event listeners.

Add RecyclerView widget to activity layout file.

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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"
    android:background="?attr/colorPrimarySurface"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/sports_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:scrollbarStyle="outsideOverlay"
        android:scrollbars="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:itemCount="12"
        tools:listitem="@layout/item_sport" />

</androidx.constraintlayout.widget.ConstraintLayout>

Now in Activity class make a query and pass it to adapter class with query parameter to listen to changes.

MainActivity.kt:

import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: SportsAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val query: Query = FirebaseFirestore.getInstance().collection("sports")

        val recyclerView: RecyclerView = findViewById(R.id.sports_list)
        adapter = SportsAdapter(query)
        recyclerView.adapter = adapter
    }

    override fun onStart() {
        super.onStart()
        adapter.startListening()
    }

    override fun onStop() {
        super.onStop()
        adapter.startListening()
    }
}

Start listening to document changes by calling adapter onStart( ) of the activity and call onStop( ) to stop listening to changes.

Now run the app, if you have implemented everything correctly you should be able to see the data.

Optional, add theme and colors to the project.

colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="blue_200">#448AFF</color>
    <color name="amber_200">#FBB255</color>
    <color name="blue_dark_primary">#141A20</color>
    <color name="blue_light_variant">#1D262E</color>
</resources>

9. Project code and helpful resources.


My next article will be on how to work with nested documents and sub collections.
That’s it guys, thanks for reading and do follow my GitHub and LinkedIn from below.



Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.