Kotlin Firestore example – CRUD Operations with FirebaseUI FirestoreRecyclerAdapter | Android

Cloud Firestore helps us store data in the cloud. It supports offline mode so our app will work fine (write, read, listen to, and query data) whether device has internet connection or not, it automatically fetches changes from our database to Firebase Server. We can structure data in our ways to improve querying and fetching capabilities. This tutorial show you an Android app that can do Firestore CRUD Operations with Android RecyclerView and FirebaseUI FirestoreRecyclerAdapter.

Related Post: Kotlin Firestore example – CRUD Operations with RecyclerView | Android

I. Technologies

– Android Studio 3
– Kotlin 1.2.0
– Firebase Firestore 11.8.0
– FirebaseUI Firestore 3.1.0

II. Overview

1. Goal

We will build an Android App that supports showing, inserting, editing, deleting Notes from/to Cloud Firestore Database with with Android RecyclerView and FirebaseUI FirestoreRecyclerAdapter:

cloud-firestore-crud-demo-show-notes

Firebase Console for Firestore will be like:

cloud-firestore-crud-demo-console-show-notes

2. Cloud Firestore

2.1 Add Firestore to Android App

Follow these steps to add Firestore to the Project.

2.2 Initialize & Reference


// Access a Cloud Firestore instance from your Activity
val db = FirebaseFirestore.getInstance()
 
// Reference to a Collection
val notesCollectionRef = db.collection("notes")
 
// Reference to a Document in a Collection
val jsaDocumentRef = db.collection("notes").document("jsa")
// or
val jsaDocumentRef = db.document("notes/jsa")
 
// Hierarchical Data with Subcollection-Document in a Document
val androidTutRef = db
        .collection("notes").document("jsa")
        .collection("tutorials").document("androidTutRef")

2.3 Add/Update/Get/Delete Data & Get Realtime Updates

Visit this part from previous Post for details.

3. FirebaseUI Firestore

To use the FirebaseUI Firestore to display list of data, we need:
– Java class for data object (Model)
– Java class for holding UI elements that match with Model’s fields (ViewHolder and layout)
– Custom RecyclerView adapter to map from a collection from Firestore to Android (FirestoreRecyclerAdapter)
RecyclerView object to set the adapter to provide child views on demand.

cloud-firestore-ui-crud-demo-show-notes

3.1 Model and ViewHolder

Model class is a class that represents the data from Firestore:


class Note {

    var id: String? = null
    var title: String? = null
    var content: String? = null

    // ...
}

ViewHolder layout (R.layout.item_note) with UI items that correspond to Model fields:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView android:id="@+id/tvTitle" />

    <TextView android:id="@+id/tvContent" />

    <ImageView android:id="@+id/ivEdit" />

    <ImageView android:id="@+id/ivDelete" />

</LinearLayout>

ViewHolder class contains Android UI fields that point to layout items:


class NoteViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    var title: TextView
    var content: TextView
    var edit: ImageView
    var delete: ImageView

    init {
        title = view.findViewById(R.id.tvTitle)
        content = view.findViewById(R.id.tvContent)

        edit = view.findViewById(R.id.ivEdit)
        delete = view.findViewById(R.id.ivDelete)
    }
}

3.2 FirestoreRecyclerAdapter subclass

We need a subclass of the FirestoreRecyclerAdapter and implement its onBindViewHolder() & onCreateViewHolder() method:


private var adapter: FirestoreRecyclerAdapter? = null
// ...
val query = firestoreDB!!.collection("notes")

val response = FirestoreRecyclerOptions.Builder()
        .setQuery(query, Note::class.java)
        .build()

adapter = object : FirestoreRecyclerAdapter(response) {

    override fun onBindViewHolder(holder: NoteViewHolder, position: Int, model: Note) {
        val note = notesList[position]
        holder.title.text = note.title
        holder.content.text = note.content
        holder.edit.setOnClickListener { updateNote(note) }
        holder.delete.setOnClickListener { deleteNote(note.id!!) }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_note, parent, false)
        return NoteViewHolder(view)
    }

    override fun onError(e: FirebaseFirestoreException?) {
        Log.e("error", e!!.message)
    }
}

Now look at these lines of code:


val query = firestoreDB!!.collection("notes")

val response = FirestoreRecyclerOptions.Builder()
        .setQuery(query, Note::class.java)
        .build()

adapter = object : FirestoreRecyclerAdapter(response) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_note, parent, false)
        return NoteViewHolder(view)
    }
}

– We tell FirestoreRecyclerAdapter object to use Note.class when reading from the Firestore database.
– Each Note will be displayed in a R.layout.item_note (that has 4 elements: tvTitle, tvContent, ivEdit, ivDelete).
– We indicate NoteViewHolder class for ViewHolder

FirebaseRecyclerAdapter will call onBindViewHolder() method for each Model it finds in database. It passes us the model and a ViewHolder.
So what we should do is map the fields from model to the correct View items:


override fun onBindViewHolder(holder: NoteViewHolder, position: Int, model: Note) {
    val note = notesList[position]
    holder.title.text = note.title
    holder.content.text = note.content
    holder.edit.setOnClickListener { updateNote(note) }
    holder.delete.setOnClickListener { deleteNote(note.id!!) }
}

3.3 RecyclerView

Now we set the adapter for RecyclerView object to provide child views on demand:


val mLayoutManager = LinearLayoutManager(applicationContext)
rvNoteList.layoutManager = mLayoutManager
rvNoteList.itemAnimator = DefaultItemAnimator()

// adapter = object : FirestoreRecyclerAdapter(response) {...}
adapter!!.notifyDataSetChanged()
rvNoteList.adapter = adapter

Remember to call adapter startListening() & stopListening() method to start/stop listening for changes in the Firestore database:


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

public override fun onStop() {
    super.onStop()
    adapter!!.stopListening()
}

3.4 Dependency

build.gradle file (App-level)


dependencies {
    // ...
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.google.firebase:firebase-firestore:11.8.0'
    implementation 'com.firebaseui:firebase-ui-firestore:3.1.0'
}

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

4. Project Structure

kotlin-firestore-ui-crud-structure

II. Practice

1. Set up Project

– Create New Project with package name com.javasampleapproach.kotlin.firebase.cloudfirestore.
– Add images (found in source code) to drawable.
– Follow these steps to add Firestore to the Project.

2. Layout

2.1 Main Activity

Open res/layout/activity_main.xml file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvNoteList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="16dp"
        android:scrollbars="vertical" />
</LinearLayout>

2.2 Item Layout

Add item_note.xml layout file to res/layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="10dp"
    android:weightSum="10">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="9"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:text="Title"
            android:textColor="@color/colorPrimary"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tvContent"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:text="Text for Content" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="right"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/ivEdit"
            android:layout_width="30dp"
            android:layout_height="30dp"
            app:srcCompat="@drawable/ic_edit" />

        <ImageView
            android:id="@+id/ivDelete"
            android:layout_width="30dp"
            android:layout_height="30dp"
            app:srcCompat="@drawable/ic_delete" />
    </LinearLayout>

</LinearLayout>

2.3 Activity Layout for Adding/Updating Note

Add activity_note.xml layout file to res/layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="15dp">

    <EditText
        android:id="@+id/edtTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:hint="Java Sample Approach"
        android:inputType="textPersonName" />

    <EditText
        android:id="@+id/edtContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:hint="Java technology, Spring Framework - approach to Java by Sample."
        android:inputType="textMultiLine" />

    <Button
        android:id="@+id/btAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Add" />
</LinearLayout>

2.4 Menu

Under res folder, create menu folder and add menu_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/addNote"
        android:icon="@drawable/ic_add"
        android:title="Add"
        app:showAsAction="ifRoom" />
</menu>

3. Data Model

Under model package:


package com.javasampleapproach.kotlin.firebase.cloudfirestore.model

import java.util.HashMap

class Note {

    var id: String? = null
    var title: String? = null
    var content: String? = null

    constructor() {}

    constructor(id: String, title: String, content: String) {
        this.id = id
        this.title = title
        this.content = content
    }

    constructor(title: String, content: String) {
        this.title = title
        this.content = content
    }

    fun toMap(): Map {

        val result = HashMap()
        result.put("title", title!!)
        result.put("content", content!!)

        return result
    }
}

4. ViewHolder

Under viewholder package, create RecyclerView.ViewHolder subclass:


package com.javasampleapproach.kotlin.firebase.cloudfirestore.viewholder

import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.javasampleapproach.kotlin.firebase.cloudfirestore.R

class NoteViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    var title: TextView
    var content: TextView
    var edit: ImageView
    var delete: ImageView

    init {
        title = view.findViewById(R.id.tvTitle)
        content = view.findViewById(R.id.tvContent)

        edit = view.findViewById(R.id.ivEdit)
        delete = view.findViewById(R.id.ivDelete)
    }
}

5. Activity

5.1 Main Activity


package com.javasampleapproach.kotlin.firebase.cloudfirestore

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.content.Intent
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.util.Log
import android.view.*
import android.widget.Toast

import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ListenerRegistration
import com.javasampleapproach.kotlin.firebase.cloudfirestore.model.Note
import kotlinx.android.synthetic.main.activity_main.*
import com.firebase.ui.firestore.FirestoreRecyclerAdapter
import com.firebase.ui.firestore.FirestoreRecyclerOptions
import com.google.firebase.firestore.FirebaseFirestoreException
import com.javasampleapproach.kotlin.firebase.cloudfirestore.viewholder.NoteViewHolder

class MainActivity : AppCompatActivity() {

    private val TAG = "MainActivity"

    private var adapter: FirestoreRecyclerAdapter? = null

    private var firestoreDB: FirebaseFirestore? = null
    private var firestoreListener: ListenerRegistration? = null
    private var notesList = mutableListOf()

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

        firestoreDB = FirebaseFirestore.getInstance()

        val mLayoutManager = LinearLayoutManager(applicationContext)
        rvNoteList.layoutManager = mLayoutManager
        rvNoteList.itemAnimator = DefaultItemAnimator()

        loadNotesList()

        firestoreListener = firestoreDB!!.collection("notes")
                .addSnapshotListener(EventListener { documentSnapshots, e ->
                    if (e != null) {
                        Log.e(TAG, "Listen failed!", e)
                        return@EventListener
                    }

                    notesList = mutableListOf()

                    for (doc in documentSnapshots) {
                        val note = doc.toObject(Note::class.java)
                        note.id = doc.id
                        notesList.add(note)
                    }

                    adapter!!.notifyDataSetChanged()
                    rvNoteList.adapter = adapter
                })
    }

    override fun onDestroy() {
        super.onDestroy()

        firestoreListener!!.remove()
    }

    private fun loadNotesList() {

        val query = firestoreDB!!.collection("notes")

        val response = FirestoreRecyclerOptions.Builder()
                .setQuery(query, Note::class.java)
                .build()

        adapter = object : FirestoreRecyclerAdapter(response) {
            override fun onBindViewHolder(holder: NoteViewHolder, position: Int, model: Note) {
                val note = notesList[position]

                holder.title.text = note.title
                holder.content.text = note.content

                holder.edit.setOnClickListener { updateNote(note) }

                holder.delete.setOnClickListener { deleteNote(note.id!!) }
            }

            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
                val view = LayoutInflater.from(parent.context)
                        .inflate(R.layout.item_note, parent, false)

                return NoteViewHolder(view)
            }

            override fun onError(e: FirebaseFirestoreException?) {
                Log.e("error", e!!.message)
            }
        }

        adapter!!.notifyDataSetChanged()
        rvNoteList.adapter = adapter
    }

    public override fun onStart() {
        super.onStart()

        adapter!!.startListening()
    }

    public override fun onStop() {
        super.onStop()

        adapter!!.stopListening()
    }

    private fun updateNote(note: Note) {
        val intent = Intent(this, NoteActivity::class.java)
        intent.putExtra("UpdateNoteId", note.id)
        intent.putExtra("UpdateNoteTitle", note.title)
        intent.putExtra("UpdateNoteContent", note.content)
        startActivity(intent)
    }

    private fun deleteNote(id: String) {
        firestoreDB!!.collection("notes")
                .document(id)
                .delete()
                .addOnCompleteListener {
                    Toast.makeText(applicationContext, "Note has been deleted!", Toast.LENGTH_SHORT).show()
                }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)

        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        if (item != null) {
            if (item.itemId == R.id.addNote) {
                val intent = Intent(this, NoteActivity::class.java)
                startActivity(intent)
            }
        }

        return super.onOptionsItemSelected(item)
    }
}

5.2 Note Activity


package com.javasampleapproach.kotlin.firebase.cloudfirestore

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast

import com.google.firebase.firestore.FirebaseFirestore
import com.javasampleapproach.kotlin.firebase.cloudfirestore.model.Note

import kotlinx.android.synthetic.main.activity_note.*

class NoteActivity : AppCompatActivity() {

    private val TAG = "AddNoteActivity"

    private var firestoreDB: FirebaseFirestore? = null
    internal var id: String = ""

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

        firestoreDB = FirebaseFirestore.getInstance()

        val bundle = intent.extras
        if (bundle != null) {
            id = bundle.getString("UpdateNoteId")

            edtTitle.setText(bundle.getString("UpdateNoteTitle"))
            edtContent.setText(bundle.getString("UpdateNoteContent"))
        }

        btAdd.setOnClickListener {
            val title = edtTitle.text.toString()
            val content = edtContent.text.toString()

            if (title.isNotEmpty()) {
                if (id.isNotEmpty()) {
                    updateNote(id, title, content)
                } else {
                    addNote(title, content)
                }
            }

            finish()
        }
    }

    private fun updateNote(id: String, title: String, content: String) {
        val note = Note(id, title, content).toMap()

        firestoreDB!!.collection("notes")
                .document(id)
                .set(note)
                .addOnSuccessListener {
                    Log.e(TAG, "Note document update successful!")
                    Toast.makeText(applicationContext, "Note has been updated!", Toast.LENGTH_SHORT).show()
                }
                .addOnFailureListener { e ->
                    Log.e(TAG, "Error adding Note document", e)
                    Toast.makeText(applicationContext, "Note could not be updated!", Toast.LENGTH_SHORT).show()
                }
    }

    private fun addNote(title: String, content: String) {
        val note = Note(title, content).toMap()

        firestoreDB!!.collection("notes")
                .add(note)
                .addOnSuccessListener { documentReference ->
                    Log.e(TAG, "DocumentSnapshot written with ID: " + documentReference.id)
                    Toast.makeText(applicationContext, "Note has been added!", Toast.LENGTH_SHORT).show()
                }
                .addOnFailureListener { e ->
                    Log.e(TAG, "Error adding Note document", e)
                    Toast.makeText(applicationContext, "Note could not be added!", Toast.LENGTH_SHORT).show()
                }
    }
}

6. Android Manifest

Define NoteActivity class as an Android Activity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.javasampleapproach.firebase.cloudfirestore">

    <application...>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".NoteActivity"></activity>
    </application>

</manifest>

IV. Source Code

KotlinFireStore-FirebaseUI-FirestoreRecyclerAdapter

5 thoughts on “Kotlin Firestore example – CRUD Operations with FirebaseUI FirestoreRecyclerAdapter | Android”

  1. In main activity, the line ‘adapter = object : FirestoreRecyclerAdapter(response)’ gives error of type mismatch. and in the code
    ‘ for (doc in documentSnapshots!!) {
    val note = doc.toObject(Note::class.java)
    note.id = doc.id
    notesList.add(note)
    } ‘ this – (note) also gives error of typemismatch. help me plz.

    1. above errors are solved, now my application is crashing with the error – ‘NoClassDefFoundError: Failed resolution of: Lcom/google/firebase/firestore/QueryListenOptions;’

  2. Thank you, I have just been looking for info about this topic for a long time and yours is the greatest I’ve discovered till now. However, what concerning the bottom line? Are you certain about the source?

Leave a Reply

Your email address will not be published. Required fields are marked *