Home > Software design >  How to use collectAsState() when getting data from Firestore?
How to use collectAsState() when getting data from Firestore?

Time:09-21

A have a screen where I display 10 users. Each user is represented by a document in Firestore. On user click, I need to get its details. This is what I have tried:

fun getUserDetails(uid: String) {
    LaunchedEffect(uid) {
        viewModel.getUser(uid)
    }
    when(val userResult = viewModel.userResult) {
        is Result.Loading -> CircularProgressIndicator()
        is Result.Success -> Log.d("TAG", "You requested ${userResult.data.name}")
        is Result.Failure -> Log.d("TAG", userResult.e.message)
    }
}

Inside the ViewModel class, I have this code:

var userResult by mutableStateOf<Result<User>>(Result.Loading)
    private set

fun getUser(uid: String) = viewModelScope.launch {
    repo.getUser(uid).collect { result ->
        userResult = result
    }
}

As you see, I use Result.Loading as a default value, because the document is heavy, and it takes time to download it. So I decided to display a progress bar. Inside the repo class I do:

override fun getUser(uid: String) = flow {
    try {
        emit(Result.Loading)
        val user = usersRef.document(uid).get().await().toObject(User::class.java)
        emit(Result.Success(user))
    } catch (e: Exception) {
        emit(Result.Failure(e))
    }
}

I have two questions, if I may.

  1. Is there something wrong with this code? As it works fine when I compile.
  2. I saw some questions here, that recommend using collectAsState() or .collectAsStateWithLifecycle(). I tried changing userResult.collectAsState() but I cannot find that function. Is there any benefit in using collectAsState() or .collectAsStateWithLifecycle() than in my actual code? I'm really confused.

CodePudding user response:

If you wish to follow enter image description here

You emit your result from Repository and handle states or change data, if you Domain Driven Model, you store DTOs for data from REST api, if you have db you keep database classes instead of passing classes annotated with REST api annotation or db annotation to UI you pass a UI.

In repository you can pass data as

override fun getUser(uid: String) = flow {
       val user usersRef.document(uid).get().await().toObject(User::class.java)
  emit(user)
}

In UseCase you check if this returns error, or your User and then convert this to a Result or a class that returns error or success here. You can also change User data do Address for instance if your business logic requires you to return an address.

If you apply business logic inside UseCase you can unit test what you should return if you retrieve data successfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java/kotlin class and unit test anywhere not only in Android studio.

In ViewModel after getting a Flow< Result<User>> you can pass this to Composable UI.

Since Compose requires a State to trigger recomposition you can convert your Flow with collectAsState to State and trigger recomposition with required data.

CollectAsState is nothing other than Composable function produceState

@Composable
fun <T : R, R> Flow<T>.collectAsState(
    initial: R,
    context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
    if (context == EmptyCoroutineContext) {
        collect { value = it }
    } else withContext(context) {
        collect { value = it }
    }
}

And produceState

@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(key1, key2) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

CodePudding user response:

As per discussion in comments, you can try this approach:

// Repository
suspend fun getUser(uid: String): Result<User> {
    return try {
        val user = usersRef.document(uid).get().await().toObject(User::class.java)
        Result.Success(user)
    } catch (e: Exception) {
        Result.Failure(e)
    }
}

// ViewModel
var userResult by mutableStateOf<Result<User>?>(null)
    private set

fun getUser(uid: String) {
    viewModelScope.launch {
        userResult = Result.Loading // set initial Loading state
        userResult = repository.getUser(uid) // update the state again on receiving the response
    }
}
  • Related