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.
- Is there something wrong with this code? As it works fine when I compile.
- I saw some questions here, that recommend using
collectAsState()or.collectAsStateWithLifecycle(). I tried changinguserResult.collectAsState()but I cannot find that function. Is there any benefit in usingcollectAsState()or.collectAsStateWithLifecycle()than in my actual code? I’m really confused.
>Solution :
If you wish to follow Uncle Bob’s clean architecture you can split your architecture into Data, Domain and Presentation layers.
For android image below shows how that onion shape can be simplified to
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 State 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 sucessfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java 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
}
