Hey people! This article helps you to understand and implement RecyclerView from Cloud Firestore in No-SQL form of collections, documents and fields.

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. Setup project in firebase console

You can refer to the source code from my Github project.

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.

2. Add dependencies and plugin

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.10'

Finally add Firestore , RecyclerView dependencies. Also make sure to add ConstraintLayout and Material dependencies, so please refer to source code.

implementation 'com.google.firebase:firebase-firestore:24.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'

3. Structure collection, documents, fields

In cloud firestore data is stored in form of documents.
Documents contain fields to hold data in form of number, string, lists etc.
All those documents with fields belong to a collection.

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.

4. Create FirestoreAdapter for listening to events.

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> {

}

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 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) {
            New Document was added to set of documents matching query.            
            DocumentChange.Type.ADDED -> onDocumentAdded(change)
            // New document within the query was modified.            
            DocumentChange.Type.MODIFIED -> onDocumentModified(change)
            // Removed, deleted, no longer matches the query.            
            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()
}

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()
    }

    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]
    }
}

5. 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.

6. 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.

7. 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>

8. 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. You can find me at GitHub and LinkedIn from below.

Rajasekhar K E


Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.

Here We Go Again : (

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

Leave a Reply

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