Jetpack Compose load Firestore Data with Flow

November 2, 2021

Create a class to handle data state (loading, error, success), as I believe it is impossible to handle exception within composable.

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

// https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T?): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(data: T?): Resource<T> {
            return Resource(Status.LOADING, data, null)
        }
    }
}

Data Class

data class Post(var id: String, var created: Timestamp? = null, var title: String? = null) {
  companion object {
    fun toObject(doc: DocumentSnapshot): Post? {
        val item = doc.toObject<Post>()
        item?.id = doc.id
        return item
    }
  }
}

ViewModel to load data from Firestore using Flow

class MainViewModel : ViewModel() {
    // consider using Dependency injection with Hilt
    private val auth: FirebaseAuth = Firebase.auth
    private val db = Firebase.firestore

    fun fetchPosts() = callbackFlow {
        val collection = db.collection("posts")

        val snapshotListener = collection.orderBy("created", Query.Direction.DESCENDING).addSnapshotListener { value, error ->

            val response = if (error == null && value != null) {
                val data = value.documents.map { doc ->
                    Post.toObject(doc)
                }
                Resource.success(data)
            } else {
                Timber.e(error)
                Resource.error(error.toString(), null)
           }

            offer(response)
        }

        awaitClose() {
            snapshotListener.remove()
        }
    }
}

Composable

@Composable
fun JourneyApp(viewModel: MainViewModel = viewModel()) {
    val postsResource by viewModel.fetchPosts().collectAsState(initial = Resource.loading(null))
    val posts = postsResource.data ?: emptyList()

    if (postsResource.status == Status.ERROR) {
        Text("Error: ${postsResource.message}")
    }
    else if (postsResource.status == Status.LOADING) {
        Text("Loading ....")
    }
    else {
        LazyColumn {
            items(posts) { post ->
                Text(post.title)
            }
        }
    }
}
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.