Initialize Firestore and save a post to the database
Video: Initialize Firestore and save a post to the database by Taught by Celeste AI - AI Coding Coach
Firestore: Initialize and Save a Post
FirebaseFirestore.getInstance()for the client.db.collection("posts").add(post)for a new auto-ID document.data class Post(...)with no-arg defaults so Firestore can deserialize it back.
A common Android task: save a structured object to Firebase Firestore, the cloud document database. Today we set up the client and write one document.
Prerequisites
Before any code, the Firebase setup:
- Create a Firebase project at console.firebase.google.com.
- Add an Android app to the project (
com.example.myapppackage name). - Download
google-services.jsonand place it inapp/directory. - Add the
google-servicesplugin and Firestore SDK to yourbuild.gradle.kts:
plugins {
id("com.google.gms.google-services")
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.5.1"))
implementation("com.google.firebase:firebase-firestore-ktx")
}
The firebase-bom (Bill of Materials) is a version-aligned bundle. Pinning the BoM keeps all Firebase SDKs at compatible versions.
The Copilot prompt
import com.google.firebase.firestore.FirebaseFirestore
// Define a Post data class and a function to save it to Firestore
data class Post(
Copilot generates:
data class Post(
val title: String = "",
val content: String = ""
)
fun savePostToFirestore() {
val db = FirebaseFirestore.getInstance()
val post = Post(
title = "Hello Firestore",
content = "This is a post saved from my Android app."
)
db.collection("posts")
.add(post)
.addOnSuccessListener { documentRef ->
println("Post saved with ID: ${documentRef.id}")
}
.addOnFailureListener { e ->
println("Error saving post: ${e.message}")
}
}
Walkthrough
1. Default values on data class fields. Firestore's deserialization needs a no-arg constructor. Kotlin data classes don't have one by default — but if every field has a default, Kotlin synthesizes one. That's why you see val title: String = "" instead of just val title: String.
2. FirebaseFirestore.getInstance(). The singleton client. Uses your google-services.json config to know which project to talk to. First call initializes; subsequent calls return the same instance.
3. db.collection("posts"). A reference to the posts collection. Doesn't actually fetch anything — just builds a query target.
4. .add(post). Creates a new document with an auto-generated ID. Firestore serializes the Post to a JSON-like document on the wire.
5. addOnSuccessListener / addOnFailureListener. Firestore's API uses Task<T> (Google's Promise-like type) — callback-style. Modern Kotlin code prefers coroutines (next section).
With coroutines
The firebase-firestore-ktx artifact ships extension functions like await() for use with coroutines:
import kotlinx.coroutines.tasks.await
suspend fun savePost(post: Post): String {
val db = FirebaseFirestore.getInstance()
val ref = db.collection("posts").add(post).await()
return ref.id
}
await() suspends the coroutine until the Task completes, returning the result or throwing on failure. Cleaner than nested callbacks.
Reading back
suspend fun getPosts(): List<Post> {
val snapshot = FirebaseFirestore.getInstance()
.collection("posts")
.get()
.await()
return snapshot.toObjects(Post::class.java)
}
snapshot.toObjects(Class) deserializes every document into your data class. Each Post instance has the field values from Firestore.
To include the document ID:
data class PostWithId(
val id: String = "",
val title: String = "",
val content: String = "",
)
val posts = snapshot.documents.map { doc ->
doc.toObject(Post::class.java)!!.let { p ->
PostWithId(id = doc.id, title = p.title, content = p.content)
}
}
The document ID isn't a field on the document itself — it's metadata. Stitch it in manually if you need it.
Listening for live updates
db.collection("posts")
.addSnapshotListener { snapshot, error ->
if (error != null) {
println("Listen failed: ${error.message}")
return@addSnapshotListener
}
val posts = snapshot?.toObjects(Post::class.java) ?: emptyList()
// update UI
}
addSnapshotListener fires once with the initial state, then again every time a document in the collection changes. Returns a ListenerRegistration; call .remove() to unsubscribe.
For a Compose-friendly Flow:
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
fun postsFlow(): Flow<List<Post>> = callbackFlow {
val reg = FirebaseFirestore.getInstance()
.collection("posts")
.addSnapshotListener { snapshot, error ->
if (error != null) close(error)
else trySend(snapshot?.toObjects(Post::class.java) ?: emptyList())
}
awaitClose { reg.remove() }
}
Then in a Composable: val posts by postsFlow().collectAsState(initial = emptyList()).
Specific document ID
To save with a known ID instead of auto-generated:
db.collection("posts").document("my-post-id").set(post)
set overwrites the document at that ID; add creates with auto-generated ID. Use set when the ID is meaningful (e.g., user UID).
For partial updates:
db.collection("posts").document("my-post-id").update("title", "New title")
update modifies just the named fields, leaving others untouched.
Common mistakes
Forgetting default values on data class. Firestore deserialization fails with "Could not deserialize object. Class Post does not define a no-argument constructor."
Mixing collection paths. collection("posts/123") is wrong — collection paths have an odd number of segments. Document paths have an even number. posts (collection) → posts/123 (document) → posts/123/comments (subcollection).
Reading without await(). Forgetting await() returns a Task<DocumentReference>, not the document. Code compiles but doesn't do what you expect.
Not handling the error case in callbacks. addOnSuccessListener without a matching addOnFailureListener silently drops failures.
Storing massive blobs. Firestore has a 1MB document limit. For files, use Cloud Storage; store only the URL in Firestore.
Forgetting security rules. By default, Firestore allows anyone to read/write. Lock down via Firestore Security Rules in the console before production.
What's next
Episode 12: What day of the week is 2023-01-01? A LocalDate puzzle that demos the dayOfWeek enum and the cleanest way to print weekday names.
Recap
Firebase + Firestore: FirebaseFirestore.getInstance() for the client. data class Post(val title: String = "", ...) with defaults so deserialization works. .collection("posts").add(post) for auto-generated IDs; .set(post) on a specific document path for known IDs. With coroutines, use .await() from kotlinx-coroutines-play-services. Real-time updates via addSnapshotListener (or wrap as a callbackFlow for Compose).
Next episode: day of the week.