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: Cloud Firestore Android example – CRUD Operations with RecyclerView
I. Technologies
– Android Studio 3
– 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 FirebaseFirestore db = FirebaseFirestore.getInstance(); // Reference to a Collection CollectionReference notesCollectionRef = db.collection("notes"); // Reference to a Document in a Collection DocumentReference jsaDocumentRef = db.collection("notes").document("jsa"); // or DocumentReference jsaDocumentRef = db.document("notes/jsa"); // Hierarchical Data with Subcollection-Document in a Document DocumentReference 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:
public class Note { private String id; private String title; private String content; // ... } |
– 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:
public class NoteViewHolder extends RecyclerView.ViewHolder { public TextView title, content; public ImageView edit; public ImageView delete; public NoteViewHolder(View view) { super(view); 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 FirestoreRecyclerAdapter adapter; // ... Query query = firestoreDB.collection("notes"); FirestoreRecyclerOptions<Note> response = new FirestoreRecyclerOptions.Builder<Note>() .setQuery(query, Note.class) .build(); adapter = new FirestoreRecyclerAdapter<Note, NoteViewHolder>(response) { @Override protected void onBindViewHolder(NoteViewHolder holder, int position, Note model) final Note note = notesList.get(position); holder.title.setText(note.getTitle()); holder.content.setText(note.getContent()); holder.edit.setOnClickListener(); holder.delete.setOnClickListener(); } @Override public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new NoteViewHolder(view); } @Override public void onError(FirebaseFirestoreException e) { Log.e("error", e.getMessage()); } }; |
Now look at these lines of code:
Query query = firestoreDB.collection("notes"); FirestoreRecyclerOptions<Note> response = new FirestoreRecyclerOptions.Builder<Note>() .setQuery(query, Note.class) .build(); adapter = new FirestoreRecyclerAdapter<Note, NoteViewHolder>(response) { @Override public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new 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 protected void onBindViewHolder(NoteViewHolder holder, int position, Note model) final Note note = notesList.get(position); holder.title.setText(note.getTitle()); holder.content.setText(note.getContent()); holder.edit.setOnClickListener(); holder.delete.setOnClickListener(); } |
3.3 RecyclerView
Now we set the adapter for RecyclerView
object to provide child views on demand:
private RecyclerView recyclerView; // ... recyclerView = findViewById(R.id.rvNoteList); RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext()); recyclerView.setLayoutManager(mLayoutManager); recyclerView.setItemAnimator(new DefaultItemAnimator()); // adapter = new FirestoreRecyclerAdapter<Note, NoteViewHolder>(response) {...} adapter.notifyDataSetChanged(); recyclerView.setAdapter(adapter); |
Remember to call adapter startListening()
& stopListening()
method to start/stop listening for changes in the Firestore database:
@Override public void onStart() { super.onStart(); adapter.startListening(); } @Override public void 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.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.firebase.cloudfirestore.model; import java.util.HashMap; import java.util.Map; public class Note { private String id; private String title; private String content; public Note() { } public Note(String id, String title, String content) { this.id = id; this.title = title; this.content = content; } public Note(String title, String content) { this.title = title; this.content = content; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("title", this.title); result.put("content", this.content); return result; } } |
4. ViewHolder
Under viewholder package, create RecyclerView.ViewHolder
subclass:
package com.javasampleapproach.firebase.cloudfirestore.viewholder; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.javasampleapproach.firebase.cloudfirestore.R; public class NoteViewHolder extends RecyclerView.ViewHolder { public TextView title, content; public ImageView edit; public ImageView delete; public NoteViewHolder(View view) { super(view); 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.firebase.cloudfirestore; import android.content.Intent; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.firebase.ui.firestore.FirestoreRecyclerAdapter; import com.firebase.ui.firestore.FirestoreRecyclerOptions; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; import com.javasampleapproach.firebase.cloudfirestore.model.Note; import com.javasampleapproach.firebase.cloudfirestore.viewholder.NoteViewHolder; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private RecyclerView recyclerView; private FirestoreRecyclerAdapter adapter; private FirebaseFirestore firestoreDB; private ListenerRegistration firestoreListener; private List<Note> notesList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.rvNoteList); firestoreDB = FirebaseFirestore.getInstance(); RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext()); recyclerView.setLayoutManager(mLayoutManager); recyclerView.setItemAnimator(new DefaultItemAnimator()); loadNotesList(); firestoreListener = firestoreDB.collection("notes") .addSnapshotListener(new EventListener<QuerySnapshot>() { @Override public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) { if (e != null) { Log.e(TAG, "Listen failed!", e); return; } notesList = new ArrayList<>(); for (DocumentSnapshot doc : documentSnapshots) { Note note = doc.toObject(Note.class); note.setId(doc.getId()); notesList.add(note); } adapter.notifyDataSetChanged(); recyclerView.setAdapter(adapter); } }); } @Override protected void onDestroy() { super.onDestroy(); firestoreListener.remove(); } private void loadNotesList() { Query query = firestoreDB.collection("notes"); FirestoreRecyclerOptions<Note> response = new FirestoreRecyclerOptions.Builder<Note>() .setQuery(query, Note.class) .build(); adapter = new FirestoreRecyclerAdapter<Note, NoteViewHolder>(response) { @Override protected void onBindViewHolder(NoteViewHolder holder, int position, Note model) { final Note note = notesList.get(position); holder.title.setText(note.getTitle()); holder.content.setText(note.getContent()); holder.edit.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { updateNote(note); } }); holder.delete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { deleteNote(note.getId()); } }); } @Override public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new NoteViewHolder(view); } @Override public void onError(FirebaseFirestoreException e) { Log.e("error", e.getMessage()); } }; adapter.notifyDataSetChanged(); recyclerView.setAdapter(adapter); } @Override public void onStart() { super.onStart(); adapter.startListening(); } @Override public void onStop() { super.onStop(); adapter.stopListening(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item != null) { if (item.getItemId() == R.id.addNote) { Intent intent = new Intent(this, NoteActivity.class); startActivity(intent); } } return super.onOptionsItemSelected(item); } private void updateNote(Note note) { Intent intent = new Intent(this, NoteActivity.class); intent.putExtra("UpdateNoteId", note.getId()); intent.putExtra("UpdateNoteTitle", note.getTitle()); intent.putExtra("UpdateNoteContent", note.getContent()); startActivity(intent); } private void deleteNote(String id) { firestoreDB.collection("notes") .document(id) .delete() .addOnCompleteListener(new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { Toast.makeText(getApplicationContext(), "Note has been deleted!", Toast.LENGTH_SHORT).show(); } }); } } |
5.2 Note Activity
package com.javasampleapproach.firebase.cloudfirestore; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FirebaseFirestore; import com.javasampleapproach.firebase.cloudfirestore.model.Note; import java.util.Map; public class NoteActivity extends AppCompatActivity { private static final String TAG = "AddNoteActivity"; TextView edtTitle; TextView edtContent; Button btAdd; private FirebaseFirestore firestoreDB; String id = ""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_note); edtTitle = findViewById(R.id.edtTitle); edtContent = findViewById(R.id.edtContent); btAdd = findViewById(R.id.btAdd); firestoreDB = FirebaseFirestore.getInstance(); Bundle bundle = getIntent().getExtras(); if (bundle != null) { id = bundle.getString("UpdateNoteId"); edtTitle.setText(bundle.getString("UpdateNoteTitle")); edtContent.setText(bundle.getString("UpdateNoteContent")); } btAdd.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String title = edtTitle.getText().toString(); String content = edtContent.getText().toString(); if (title.length() > 0) { if (id.length() > 0) { updateNote(id, title, content); } else { addNote(title, content); } } finish(); } }); } private void updateNote(String id, String title, String content) { Map<String, Object> note = (new Note(id, title, content)).toMap(); firestoreDB.collection("notes") .document(id) .set(note) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { Log.e(TAG, "Note document update successful!"); Toast.makeText(getApplicationContext(), "Note has been updated!", Toast.LENGTH_SHORT).show(); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.e(TAG, "Error adding Note document", e); Toast.makeText(getApplicationContext(), "Note could not be updated!", Toast.LENGTH_SHORT).show(); } }); } private void addNote(String title, String content) { Map<String, Object> note = new Note(title, content).toMap(); firestoreDB.collection("notes") .add(note) .addOnSuccessListener(new OnSuccessListener<DocumentReference>() { @Override public void onSuccess(DocumentReference documentReference) { Log.e(TAG, "DocumentSnapshot written with ID: " + documentReference.getId()); Toast.makeText(getApplicationContext(), "Note has been added!", Toast.LENGTH_SHORT).show(); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.e(TAG, "Error adding Note document", e); Toast.makeText(getApplicationContext(), "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
FirebaseCloudFireStore-FirebaseUI-CRUD
Last updated on July 13, 2018.
I get this error:
java.lang.NoClassDefFoundError: Failed resolution of: Lcom/google/firebase/firestore/QueryListenOptions;
at com.firebase.ui.firestore.FirestoreRecyclerOptions$Builder.setQuery(FirestoreRecyclerOptions.java:117)
i get the same error