androidkotlingoogle-cloud-firestoreandroid-livedatakotlin-multiplatform

How to use collectAsState in an KMM app with ViewModel arhitecture


How to use collectAsState in an KMM app? Currently I use it in my app like this:

UI:

usersViewModel.userInfo.collectAsState(null).apply {
//use data from userInfo
}

ViewModel:

 val userInfo = repo.getUserProfile().catch { println("no User found") }

Repo:

fun getUserProfile(): Flow<User?> = flow {
    try {
        firestore.collection("User").document(auth.currentUser!!.uid).snapshots.collect { user ->
            emit(user.data<UserInfo>())
        }
    }catch (ex: Exception){
        println("Something wrong")
    }
}

fun getAllRestaurants(): Flow<List<Restaurant>> {
    val user = getUserProfile()
    firestore.collection("Restaurant").snapshots.map { querySnapshot ->
        val restaurants = querySnapshot.documents.map { documentSnapshot ->
            documentSnapshot.data<Restaurant>()
        }.filter { restaurant ->
            checkIfPointInsidePolygon(user.first()!!.userLocation!!, restaurant.polygonList)
        }
        restaurants
    }
}

It seems quite slow at the moment and I do not know if it is because of the debug or I'm not getting the live data in the best way. Could you verify also if getAllRestaurants is done in the right way for a list of object?

Thanks for the help!


Solution

  • The general idea is not to collect the flows, instead just pass them through the layers (possibly transforming them on the way) until they arrive in the UI. Only there they will finally be collected.

    For your repository that means you shouldn't collect and re-emit the value, instead just transform the flow by mapping its content:

    fun getUserProfile(): Flow<User?> =
        firestore.collection("User").document(auth.currentUser!!.uid).snapshots
            .mapLatest { user ->
                user.data<UserInfo>()
            }
            .catch { ex ->
                println("Something wrong")
            }
    

    In the view model you need to convert the flow to a StateFlow:

    val userInfo: StateFlow<User?> = repo.getUserProfile()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = null,
        )
    

    And finally in your composable you convert the StateFlow into a Compose State:

    val userInfo: User? by usersViewModel.userInfo.collectAsState()
    

    (If you want the collection to be lifecycle-aware you need to use collectAsStateWithLifecycle from the gradle dependency androidx.lifecycle:lifecycle-runtime-compose instead.)

    Note the by delegation, that lets you use userInfo as though it would be of type User?, whereas it actually is a State<User?> under the hood.

    Now you can do whatever you want with the userInfo variable. The Flow framework guarantees that this variable will always be up-to-date and a recomposition of your UI is triggered whenever a new value comes along.