1

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
Joan P.
  • 2,368
  • 6
  • 30
  • 63
  • 1
    I wish i had seen this question before :). Anyways there are good answers already. You just need to convert your `Flow` into `State` using Flow extension `collectAsState`. If you don't make request as soon as Composable enters composition you can consider another state for nothing being happening. People don't want to see a progress bar indefinitely if you are not requesting anything without user interaction – Thracian Sep 20 '22 at 15:05
  • @Thracian So is it really necessary to use collectAsState, since I don't collect the response inside a composable function but inside the ViewModel class? – Joan P. Sep 20 '22 at 15:09
  • 1
    It's not necessary but easiest way. You can also create a `MutableState` instance inside your ViewModel and update this `MutableState` using `flow.collect{myState =it}`or `flow.onEach{myState =it}`. If you wish to don't have a `MutableState` inside `ViewModel` use `produceState` in a Compoable which converts `Flow` into a MutableState as i answered in your other question. You need a Composa `State` to trigger recomposition. You can check out this question for why state is needed. https://stackoverflow.com/q/65368007/5457853 – Thracian Sep 20 '22 at 15:13

2 Answers2

4

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

sergpetrov
  • 1,506
  • 1
  • 11
  • 20
Rohan Sharma
  • 374
  • 4
  • 11
  • Thanks, Rohan, for taking the time to provide and answer. Yes, it's a nice approach, but it doesn't really answer my question regarding the use of the flow. Just voted-up. – Joan P. Sep 17 '22 at 11:11
  • I have updated my answer can u check if you got what u want ? – Rohan Sharma Sep 17 '22 at 11:18
  • As I already said, it's a nice approach, but my question is, is it a bad approach to add Loading to a Flow? Or is it better to not to use flow? – Joan P. Sep 17 '22 at 11:53
  • first thing u will be usiing mutableStateOf instead of flow. Now in that u need to pass ModelStateName which will contain loading sucess and failure and u need ti toggle these states from viewmodel. Loading will be deafault state. Flow is not lifecycle aware so we cant use in compose functions mutableStateOf. is lfecycle aware and can be used in compose functions and is recomended way – Rohan Sharma Sep 17 '22 at 12:08
  • I'm already using a state object. incrementResponse is such an object. Please check my updated question. – Joan P. Sep 17 '22 at 12:16
  • Basically, my question is, is it ok to use a flow to display the ProgressBar or should I handle that inside de composable screen. That's a follow up question of this [question](https://stackoverflow.com/a/73736263/7919372). – Joan P. Sep 17 '22 at 12:20
  • it depends on ur usecase if its a one time call then flow is not required u can go with suspend function however if ur usecase asks u to get the updates continously then u need to use flow in that case. This is my understanding. if this doesnt help u might be I am unable to get the context in which u r trying to ask so some other answer might help!! – Rohan Sharma Sep 17 '22 at 12:27
  • Yes, the incrementation can occur each time the user wants to increment the quantity. – Joan P. Sep 17 '22 at 12:28
  • https://youtu.be/qvDo0SKR8-k u can go through this video this has an increment example. u will find the same usecase in this video – Rohan Sharma Sep 17 '22 at 12:32
  • I'm not asking here regarding an increment operation, but any other operation that can take place using a flow. – Joan P. Sep 17 '22 at 12:43
3

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()
}
Arpit Shukla
  • 9,612
  • 1
  • 14
  • 40
  • Thank you, for taking the time to answer my question. Let me take a look and get back to you. – Joan P. Sep 20 '22 at 11:44
  • When I'm suing the latest approach, the Progress Bar starts spinning even from the beginning, as it has an initial state of Loading. I only want to be displayed when I click the increment button, until the increment operation is committed. – Joan P. Sep 20 '22 at 12:20
  • Besides that, you're using `private val _incrementResultFlow = MutableStateFlow(Result.Loading)` and `val incrementResultFlow = _incrementResultFlow.asStateFlow()` inside the VIewMode class. Can this be simplified with something like `var incrementResponse by mutableStateOf(Result.Loading) private set`? – Joan P. Sep 20 '22 at 12:41
  • For your 2nd comment, yes the second approach is actually shorter, that's why I use it in my projects. You can go with that. – Arpit Shukla Sep 20 '22 at 12:46
  • I have edited the answer to handle your requirement that you mentioned in 1st comment. Please take a look. – Arpit Shukla Sep 20 '22 at 12:49
  • Yes, but is there any to use a `MutableStateFlow` with a private `set`? As it works in case of `mutableStateOf`? – Joan P. Sep 20 '22 at 13:04
  • 1
    In case of `mutableStateOf` private `set` is for the backing field that we are using (notice the `by` delegation). Using private setter with `MutableStateFlow` doesn't make sense because we aren't going to change that flow variable anyway, what can be changed is `flow.value` property. – Arpit Shukla Sep 20 '22 at 13:58
  • Thanks. I just added a follow up [question](https://stackoverflow.com/questions/73788175/how-to-use-collectasstate-when-getting-data-from-firestore). Maybe you can take a look. – Joan P. Sep 20 '22 at 14:15
  • The link to question is incorrect. It is pointing to my profile right now. – Arpit Shukla Sep 20 '22 at 14:17
  • Sorry, I just edited that. Please check. – Joan P. Sep 20 '22 at 14:18
  • Hey, sorry I misunderstood the question a little bit. I thought the `incrementQuantity` function is in your viewModel. Since it is a repo function, you can choose to keep returning the flow from there. And then you can collect it in your viewModel to update the state (compose `State` or `StateFlow`) – Arpit Shukla Sep 20 '22 at 14:26
  • No, `incrementQuantity` is inside the repository class. So, you say, that there is nothing wrong in emitting the `Loading` state for the first time, and then when the data becomes available, emit the result in the repo class. Is that correct? – Joan P. Sep 20 '22 at 14:31
  • Yes, you can do that. But as a personal preference, I don't like returning flow from repository just to handle this Loading state. I do this in the viewModel itself and keep the repository function a normal `suspend` function. But it's all up to you. (Let me add this approach too in the answer, you can choose whatever you like the best) – Arpit Shukla Sep 20 '22 at 14:35
  • Looking forward to seeing it. Thanks again. – Joan P. Sep 20 '22 at 14:39
  • Posted it on the other question. Have a look. – Arpit Shukla Sep 20 '22 at 14:42
  • Let me take a look and get back to you. – Joan P. Sep 20 '22 at 14:48
  • If you can adapt the answer according to my question, so feature visitors will benefit from this, I will accept the answer and reward you with the bounty. – Joan P. Sep 20 '22 at 15:01
  • 1
    Sure. Give me a minute. – Arpit Shukla Sep 20 '22 at 15:02
  • Edited. See if it looks good to you. – Arpit Shukla Sep 20 '22 at 16:03
  • Hey. Maybe you can help me with [that](https://stackoverflow.com/questions/73853421/how-to-stop-collectaslazypagingitems-from-firing-when-itemcount-is-0) too. – Joan P. Sep 28 '22 at 11:59