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
:
Firebase Console for Firestore will be like:
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.
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<Note, NoteViewHolder>? = null // ... val query = firestoreDB!!.collection("notes") val response = FirestoreRecyclerOptions.Builder<Note>() .setQuery(query, Note::class.java) .build() adapter = object : FirestoreRecyclerAdapter<Note, NoteViewHolder>(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<Note>() .setQuery(query, Note::class.java) .build() adapter = object : FirestoreRecyclerAdapter<Note, NoteViewHolder>(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<Note, NoteViewHolder>(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
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<String, Any> { val result = HashMap<String, Any>() 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<Note, NoteViewHolder>? = null private var firestoreDB: FirebaseFirestore? = null private var firestoreListener: ListenerRegistration? = null private var notesList = mutableListOf<Note>() 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<Note>() .setQuery(query, Note::class.java) .build() adapter = object : FirestoreRecyclerAdapter<Note, NoteViewHolder>(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
Last updated on July 13, 2018.
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.
above errors are solved, now my application is crashing with the error – ‘NoClassDefFoundError: Failed resolution of: Lcom/google/firebase/firestore/QueryListenOptions;’
I have a sort button, on click of which i need to change the query dynamically. How to achieve this?