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 shows you an Android app that can do Firestore CRUD Operations with RecyclerView
.
Related Post: Cloud Firestore Android example – CRUD Operations with FirebaseUI FirestoreRecyclerAdapter
I. Technologies
– Android Studio 3
– Firebase Firestore 11.8.0
II. Overview
1. Goal
We will build an Android App that supports showing, inserting, editing, deleting Notes from/to Cloud Firestore Database with RecyclerView
:
Firebase Console for Firestore will be like:
2. Cloud Firestore
2.1 Add Firestore to Android App
– Create Android Project, then go to Firebase Console, create Firebase Project:
– When your app is created on Firebase Console, you will need to register Android Project. Now fill project package name to register your app:
– Download google-service.json config file and follow instructions:
– Add Service plugin for Gradle:
– Open build.gradle (App-level), add dependency:
dependencies { // ... implementation 'com.android.support:recyclerview-v7:26.1.0' implementation 'com.google.firebase:firebase-firestore:11.8.0' } apply plugin: 'com.google.gms.google-services' |
– We also need Authentication to work with Firestore. To simplify this example without Authentication steps, we will allow anyone be able to read or write to the database:
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 Data
Assume that our database structure is like:
notes_colection | |-----note-id_document | | | |------title_field | |------content_field | |-----note-id_document | | | |------title_field | |------content_field |
– using set()
to create or overwrite a single document:
// Option 1: Note noteData = ... db.collection("notes") .document("note-id") .set(noteData) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { Log.d(TAG, "DocumentSnapshot successfully written!"); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "Error writing document", e); } }); // Option 2: Map<String, Object> noteDataMap = new HashMap<>(); // noteDataMap.put("title", "JSA Tutorials"); // noteDataMap.put("content", "For Android Devs"); db.collection("notes") .document("note-id") .set(noteDataMap) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { Log.d(TAG, "DocumentSnapshot successfully written!"); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "Error writing document", e); } }); // auto-generate ID DocumentReference noteDocRef = db.collection("notes").document(); noteDocRef.set(noteData/noteDataMap); |
– using add()
, Cloud Firestore will auto-generate an ID:
db.collection("notes") .add(noteData/noteDataMap) .addOnSuccessListener(new OnSuccessListener<DocumentReference>() { @Override public void onSuccess(DocumentReference documentReference) { Log.d(TAG, "DocumentSnapshot written with ID: " + documentReference.getId()); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "Error adding document", e); } }); |
2.4 Get Data
– get a Document from Collection:
DocumentReference docRef = db.collection("notes").document("note-id"); docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() { @Override public void onComplete(@NonNull Task<DocumentSnapshot> task) { if (task.isSuccessful()) { DocumentSnapshot document = task.getResult(); if (document != null) { Log.d(TAG, "DocumentSnapshot data: " + task.getResult().getData()); } else { Log.d(TAG, "No such document"); } } else { Log.d(TAG, "get failed with ", task.getException()); } } }); // custom object DocumentReference docRef = db.collection("notes").document("note-id"); docRef.get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() { @Override public void onSuccess(DocumentSnapshot documentSnapshot) { Note note = documentSnapshot.toObject(Note.class); } }); |
– get all Documents from Collection:
db.collection("notes") .get() .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() { @Override public void onComplete(@NonNull Task<QuerySnapshot> task) { if (task.isSuccessful()) { for (DocumentSnapshot document : task.getResult()) { Log.d(TAG, document.getId() + " => " + document.getData()); } } else { Log.d(TAG, "Error getting documents: ", task.getException()); } } }); |
2.5 Delete Data
db.collection("notes").document("note-id") .delete() .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { Log.d(TAG, "DocumentSnapshot successfully deleted!"); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "Error deleting document", e); } }); |
2.6 Get Realtime Updates
db.collection("notes") .addSnapshotListener(new EventListener<QuerySnapshot>() { @Override public void onEvent(@Nullable QuerySnapshot snapshots, @Nullable FirebaseFirestoreException e) { if (e != null) { Log.w(TAG, "listen:error", e); return; } List<Note> notesList = new ArrayList<>(); for (DocumentSnapshot doc : snapshots) { Note note = doc.toObject(Note.class); note.setId(doc.getId()); notesList.add(note); } // instead of simply using the entire query snapshot // see the actual changes to query results between query snapshots (added, removed, and modified) for (DocumentChange dc : snapshots.getDocumentChanges()) { switch (dc.getType()) { case ADDED: Log.d(TAG, "New city: " + dc.getDocument().getData()); break; case MODIFIED: Log.d(TAG, "Modified city: " + dc.getDocument().getData()); break; case REMOVED: Log.d(TAG, "Removed city: " + dc.getDocument().getData()); break; } } } }); |
When no longer listening to data, we must detach listener so that event callbacks stop getting called:
Query query = db.collection("notes"); ListenerRegistration registration = query.addSnapshotListener( new EventListener<QuerySnapshot>() { // ... }); // ... // Stop listening to changes registration.remove(); |
3. Project Structure
III. 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. RecyclerView Adapter Class
To work with RecyclerView
, we will:
– create RecyclerView.Adapter
subclass
– create ViewHolder
class
To support edit Note and delete Note, we register a callback for ivEdit and ivDelete ImageView
.
To transfer data between 2 Activity, we use Intent
.
Under adapter package:
package com.javasampleapproach.firebase.cloudfirestore.adapter; import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.FirebaseFirestore; import com.javasampleapproach.firebase.cloudfirestore.NoteActivity; import com.javasampleapproach.firebase.cloudfirestore.R; import com.javasampleapproach.firebase.cloudfirestore.model.Note; import java.util.List; public class NoteRecyclerViewAdapter extends RecyclerView.Adapter<NoteRecyclerViewAdapter.ViewHolder> { private List<Note> notesList; private Context context; private FirebaseFirestore firestoreDB; public NoteRecyclerViewAdapter(List<Note> notesList, Context context, FirebaseFirestore firestoreDB) { this.notesList = notesList; this.context = context; this.firestoreDB = firestoreDB; } @Override public NoteRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_note, parent, false); return new NoteRecyclerViewAdapter.ViewHolder(view); } @Override public void onBindViewHolder(NoteRecyclerViewAdapter.ViewHolder holder, int position) { final int itemPosition = position; final Note note = notesList.get(itemPosition); 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(), itemPosition); } }); } @Override public int getItemCount() { return notesList.size(); } public class ViewHolder extends RecyclerView.ViewHolder { TextView title, content; ImageView edit; ImageView delete; ViewHolder(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); } } private void updateNote(Note note) { Intent intent = new Intent(context, NoteActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("UpdateNoteId", note.getId()); intent.putExtra("UpdateNoteTitle", note.getTitle()); intent.putExtra("UpdateNoteContent", note.getContent()); context.startActivity(intent); } private void deleteNote(String id, final int position) { firestoreDB.collection("notes") .document(id) .delete() .addOnCompleteListener(new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { notesList.remove(position); notifyItemRemoved(position); notifyItemRangeChanged(position, notesList.size()); Toast.makeText(context, "Note has been deleted!", Toast.LENGTH_SHORT).show(); } }); } } |
5. Activity
5.1 Main Activity
To work with RecyclerView
, we will:
– set LayoutManager
– set adapter
To work with Add Note button on Menu, we will:
– override onCreateOptionsMenu()
function to specify menu layout resource
– override onOptionsItemSelected
function to launch NoteActivity
To display List of Items, we will create loadNotesList()
and call it inside onCreate()
.
To update data change, we register a Listener
and remove it in onDestroy()
.
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.Menu; import android.view.MenuItem; 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.QuerySnapshot; import com.javasampleapproach.firebase.cloudfirestore.adapter.NoteRecyclerViewAdapter; import com.javasampleapproach.firebase.cloudfirestore.model.Note; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private RecyclerView recyclerView; private NoteRecyclerViewAdapter mAdapter; private FirebaseFirestore firestoreDB; private ListenerRegistration firestoreListener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.rvNoteList); firestoreDB = FirebaseFirestore.getInstance(); 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; } List<Note> notesList = new ArrayList<>(); for (DocumentSnapshot doc : documentSnapshots) { Note note = doc.toObject(Note.class); note.setId(doc.getId()); notesList.add(note); } mAdapter = new NoteRecyclerViewAdapter(notesList, getApplicationContext(), firestoreDB); recyclerView.setAdapter(mAdapter); } }); } @Override protected void onDestroy() { super.onDestroy(); firestoreListener.remove(); } private void loadNotesList() { firestoreDB.collection("notes") .get() .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() { @Override public void onComplete(@NonNull Task<QuerySnapshot> task) { if (task.isSuccessful()) { List<Note> notesList = new ArrayList<>(); for (DocumentSnapshot doc : task.getResult()) { Note note = doc.toObject(Note.class); note.setId(doc.getId()); notesList.add(note); } mAdapter = new NoteRecyclerViewAdapter(notesList, getApplicationContext(), firestoreDB); RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext()); recyclerView.setLayoutManager(mLayoutManager); recyclerView.setItemAnimator(new DefaultItemAnimator()); recyclerView.setAdapter(mAdapter); } else { Log.d(TAG, "Error getting documents: ", task.getException()); } } }); } @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); } } |
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
Last updated on July 31, 2018.
Hi,
Realtime Database seems to work offline just fine, but I notice many people say Firestore’s offline capabilities exceed that of the Realtime Database. How do you think its better?
Hi Eric,
Both have mobile-first, realtime SDKs and support local data storage for offline-ready apps.
But Firebase Realtime Database gives offline support for mobile clients on iOS and Android only, and Firestore supports iOS, Android, web clients.
Regards,
JSA.
I m using this example when I receive double vale from firestore it shows an error double cant converted in object. I m getting error at :
Note note = doc.toObject(Note.class)….
Hi Swapnil,
Make sure that you import the right namespace for
Note
model class.Please give me more details about your problem.
Regards,
JSA.
this is my model class
and i got the error here:
And in firestore Amount = 157.56 (String) and here i m also set number data type then i got null value for amount
What is the purpose of the codes below
in the main activity bearing in mind that we have method
How to access the auto generated note_id when we click on any item of the recyclerview