How to correctly use flow and ProgressBar in Jetpack Compose?

Viewed 118

I have this code that increments a value in the database:

override fun incrementQuantity() = flow {
    try {
        emit(Result.Loading)
        heroIdRef.update("quantity", FieldValue.increment(1)).await()
        emit(Result.Success(true))
    } catch (e: Exception) {
        emit(Result.Failure(e))
    }
}

This function is called from within a ViewModel class, and then from a composable function I read the response like this:

when(val response = viewModel.incrementResponse) {
    is Result.Loading -> showProgressBar()
    is Result.Success -> Log.d("TAG", "Quantity incremented.")
    is Result.Failure -> Log.d("TAG", response.e.message)
}

This means that in the flow I receive two events, the first one is Loading, and the second one is the data or a failure. My question is:

Is it recommended to do it that way? Or is it better to not using a flow and have something like this:

override fun incrementQuantity(): Result<Boolean> {
    try {
        heroIdRef.update("quantity", FieldValue.increment(1)).await()
        Result.Success(true)
    } catch (e: Exception) {
        Result.Failure(e)
    }
}

And inside the composable:

showProgressBar()
when(val response = viewModel.incrementResponse) {
    is Result.Success -> {
        hideProgressBar()
        Log.d("TAG", "Quantity incremented.")
    }
    is Result.Failure -> {
        hideProgressBar()
        Log.d("TAG", response.e.message)
    }
}

Are there any downsides when using Kotlin flow?

Here is my incrementResponse object:

var incrementResponse by mutableStateOf<Response<Boolean>>(Response.Success(false))
    private set
2 Answers

In jetpack compose you don't need to toggle visibility manually. You can just play with the states. You can create a separate progress bar component that can be used for other Screens as well. Below code will only get triggered if the state is loading if the state changes it will get automatically hidden

@Composable
fun CircularIndeterminateProgressBar(
uiState: UiState
 ) {
if (uiState is UiState.Loading) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        CircularProgressIndicator()// provided by compose library 
    }
}

}

and then call this component from your main screen composable function and pass state inside it

      CircularIndeterminateProgressBar(uiState = uiState)
        uiState you will get from viewmodel

In viewmodel you will use

   private val _uiState = mutableStateOf<ModelStateName>()

the above is mutable state which is lifecycle aware and will be listed in composable function directly

           val uiState by remember {
        viewmodel.uiState
    }

Read more about mutable states here mutable states in jetpack compose

Jetpack compose workshop with increment example jetpack compose workshop

It is ok to return flow from the repository. You can collect it in the ViewModel and update the state accordingly.

Also note that Compose follows a declarative UI model, so you shouldn't be calling showProgressBar() when state is Result.Loading. Instead you should just call the progress bar composable. Similarly for the other two cases of Success and Failure you should place the necessary UI elements there. It will be something like:

// In your composable function
val result = yourFlow.collectAsState()
when(result) {
    Result.Loading -> ProgressBarComposable()
    Result.Success -> SuccessComposable()
    Result.Failure -> FailureComposable()
}

Using StateFlow, you can do it like this:

// In ViewModel:
private val _incrementResultFlow = MutableStateFlow<Result<Boolean>?>(null) // Set the initial value to be null to avoid a Progress Bar in the beginning
val incrementResultFlow = _incrementResultFlow.asStateFlow()

fun onIncrementButtonClick() {
    repository.incrementQuantity().collect { result->
        _incrementResultFlow.value = result
    }
}

// In your composable
val result by viewModel.incrementResultFlow.collectAsState()
if(result != null) {
    when(result) {
       ...
    }
}

An alternative to StateFlow is directly using the compose State inside your ViewModel. If, in your project, you don't mind putting compose dependencies in your ViewModel, I will suggest using this approach.

// In ViewModel:
var incrementResponse by mutableStateOf<Result<Boolean>?>(null)
    private set // So that it can only be modified from the ViewModel

fun onIncrementButtonClick() {
    repository.incrementQuantity().collect { result->
        incrementResponse = result
    }
}

// In your composable
viewModel.incrementResponse?.let {
    when(it) {
        Result.Loading -> ProgressBarComposable()
        Result.Success -> SuccessComposable()
        Result.Failure -> FailureComposable()
    }
}

Another alternative is to modify your repository function to return a Result<Boolean> instead of the flow and handle the Loading logic in the ViewModel.

// Repository
suspend fun incrementQuantity(): Result<Boolean> {
    return try {
        heroIdRef.update("quantity", FieldValue.increment(1)).await()
        Result.Success(true)
    } catch (e: Exception) {
        Result.Failure(e)
    }
}

// ViewModel
var incrementResponse by mutableStateOf<Result<Boolean>?>(null)
    private set

fun onIncrementButtonClick() {
    incrementResponse = Result.Loading
    incrementResponse = repository.incrementQuantity()
}
Related