Firebase Compose makes it simple to bind data from Cloud Firestore to your app's UI.
Before using this library, you should be familiar with the following topics:
Suppose you have an app that displays a list of snacks and each snack is a document
in the snacks
collection of your database. In your app, you may represent a snack like this:
data class Snack(
val name: String = ",
val price: Long = 0L,
// Default values are needed for Firestore
)
For a model class with default values like the Snack
class above, Firestore can perform automatic
serialization in DocumentReference#set()
and automatic deserialization in
DocumentSnapshot#toObject()
. For more information on data mapping in Firestore, see the
documentation on custom objects.
On the main screen of your app, you may want to show the cheapest 20 snacks. In Firestore, you would use the following query:
val query = Firebase.firestore.collection("snacks")
.orderBy("price")
.limit(20)
To retrieve this data without FirebaseUI, you might use addSnapshotListener
to listen for
live query updates:
query.addSnapshotListener { snapshot, e ->
e?.let {
// Handle error
return@addSnapshotListener
}
val chats = snapshot.toObjects<Snack>()
// Update UI
// ...
}
If you're displaying a list of data, you likely want to bind the Snack
objects to one of the
Lazy
Composables, such as LazyColumn
or LazyRow
.
Firebase Compose can help you do this instantly!
The CollectionState
binds a Query
to a FirestoreCollection
sealed class, which has 3 finite states:
FirestoreCollection.Snapshot
- contains the list of documents (asDocumentSnapshot
) returned from that query.FirestoreCollection.Error
- contains the error returned from the query.FirestoreCollection.Loading
- indicates that no data or error was emitted from the query yet.
When documents are added, removed, or change these updates are automatically applied to your UI in real time.
First, create the state using the destructuring declaration provided by the library and the
remember { }
function to handle recompositions:
val lifecycleOwner = this@MainActivity // You can also pass a fragment to it
val (result) = remember { collectionStateOf(query, lifecycleOwner) }
Now you can get the list of snacks by checking the result
state and passing the result.list
property
to the items()
extension function of a LazyColumn
or LazyRow
composable:
if (result is FirestoreCollection.Snapshot) {
LazyColumn {
items(result.items) { documentSnapshot ->
// parse the DocumentSnapshot to your custom class
val snack = documentSnapshot.toObject<Snack>()!!
// Pass the snack object to your Item Composable
// ...
}
}
}
And that's it! It's that easy!
The CollectionState
uses a snapshot listener to monitor changes to the Firestore query.
To begin listening for data, call the startListening()
method. You may want to call this
in your onStart()
method. Make sure you have finished any authentication necessary to read the
data before calling startListening()
or your query will fail.
val state = collectionStateOf(query)
override fun onStart() {
super.onStart()
state.startListening()
}
Similarly, the stopListening()
call removes the snapshot listener.
Call this method when the containing Activity or Fragment stops:
override fun onStop() {
super.onStop()
state.stopListening()
}
If you don't want to manually start/stop listening you can use
Android Architecture Components to automatically manage the lifecycle of the
CollectionState
. Pass a LifecycleOwner
to it and Firebase Compose will automatically
start and stop listening in onStart()
and onStop()
.
If you would like to handle all the possible states, you can use a when
expression:
val (result) = remember { collectionStateOf(query, lifecycleOwner) }
when (result) {
is FirestoreCollection.Snapshot -> {
val items = result.items
// call a Composable with this value
}
is FirestoreCollection.Error -> {
val exception = result.exception
// call a Composable to tell the user an error occurred
}
is FirestoreCollection.Loading -> {
// call a Composable that shows a loading state
}
}
Coming Soon.
Now if you need to display a single Snack
in your app, you can use DocumentState
.
DocumentState
binds a DocumentReference
to a FirestoreDocument
sealed class, which has 3 finite states:
FirestoreDocument.Snapshot
- containsDocumentSnapshot
returned from that document reference.FirestoreDocument.Error
- contains any exception thrown when reading the document reference.FirestoreDocument.Loading
- indicates that no data or error was emitted from the document reference yet.
Its usage is similar to CollectionState
. The main difference is that DocumentState
returns a single DocumentSnapshot
instead of List<DocumentSnapshot>
:
// getting a reference to the snack with key/id 'snack1'
val documentRef = Firebase.firestore.collection("snacks").document("snack1")
val (result) = remember { documentStateOf(documentRef, lifecycleOwner) }
when (result) {
is FirestoreDocument.Snapshot -> {
val items = result.snapshot
// call a Composable with this value
}
is FirestoreDocument.Error -> {
val exception = result.exception
// call a Composable to tell the user an error occurred
}
is FirestoreDocument.Loading -> {
// call a Composable that shows a loading state
}
}
This README file was inspired by FirebaseUI.