0

Still new to compose + kotlin, so I'm having some trouble getting stateflow working. There might be some fundamentals that I don't understand or missing some function.

Problem: I have two stateflows in my view model and both would trigger a recomposition of the other one.

ViewModel:

private val _networkUiState = MutableStateFlow<NetworkUIState>(NetworkUIState.Empty)
val networkUIState: StateFlow<NetworkUIState> = _networkUiState.asStateFlow()

private val _uiState = MutableStateFlow<UIState>(UIState.Empty)
val uiState: StateFlow<UIState> = _uiState.asStateFlow()

fun someAPICall(

) {
        _networkUiState.value = NetworkUIState.Loading
        networkJob = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
            try {
                val response = repo.apiCall()
                withContext(Dispatchers.Main) {
                    _networkUiState.value = NetworkUIState.Loaded
                    _uiState.value = UIState.DoSomething(response)
                }
            } catch (error: NetworkException) {
                _networkUiState.value = NetworkUIState.NetworkError(error)
            }
        }
}

//exceptionHandler calls _networkUIState.value = NetworkUIState.NetworkError(some error) as well

Compose:

val networkUIState = viewModel.networkUIState.collectAsState().value
val uiState = viewModel.uiState.collectAsState().value

Column() {
    //...
    //UI code
    when (uiState) {
         is UIState.DoSomething -> {
             //read uiState.response and do something
         }
         is UIState.DoAnotherThing -> {
             //read response and do something else
         }
    }
    when (networkUIState) {
         is NetworkUIState.Error -> {
              //display network error
         }
         is NetworkUIState.Loading -> {
              //display loading dialog
         }
         else -> {}
    }
}

What happens:

1st time calling api:

  1. NetworkUIState triggers loading (display loading)
  2. NetworkUIState triggers loaded (hides loading)
  3. uiState triggers DoSomething with response data

2nd time calling api:

  1. NetworkUIState triggers loading (display loading)
  2. uiState triggers DoSomething with response data (from last call)
  3. NetworkUIState triggers loaded (hides loading)
  4. uiState triggers DoSomething with response data (new data)

I understand this is because of the recomposition of NetworkUiState before UIState but UIState still has the old value. My question is how can I avoid this when I absolutely need to separate these 2 states at least in the view model?

Slodin
  • 77
  • 1
  • 9
  • What are you doing in your Composition when you get UiState.DoSomething? Are you breaking the cardinal rule of **no side effects**? That’s the only reason I can think it would even be a problem for them to cause recomposition on each other. Your composition should be designed as if you could get recomposed many times without changes to the state and it wouldn’t cause any problems. – Tenfour04 Nov 27 '22 at 13:54
  • UiState.DoSoemthing, right now is just a Log.d to see whats going on. The recomposition is normal, Thracian's answer is what I had initially thought of, but I'm looking for something better. – Slodin Nov 27 '22 at 22:08
  • But why is it a problem if it gets triggered by the other state changing? This is how everything in Compose works. You must design it so it doesn’t matter (no side effects). – Tenfour04 Nov 28 '22 at 01:01
  • I know it's how compose works as I stated in my post. I'm simply asking if there is a better solution to the problem at hand (Thracian had the right idea, which is what I initially thought to do). Is there something that can combine the two stateflows in compose but still keep them separate in the view model? – Slodin Nov 28 '22 at 09:39
  • What I’m not clear on is what you’re actually doing that would be a problem if it got called repeatedly due to recompositions. You’re not supposed to do that kind of thing in the first place. Without understanding that, it’s hard to suggest how to solve it. Right now it’s a Log call which wouldn’t cause a problem. If there is a side effect that should occur only once when a specific state changes, that should be wrapped in a LaunchedEffect if you cannot remove it from the composition entirely. – Tenfour04 Nov 28 '22 at 12:47
  • There aren't any side effects going on, again, I don't know how to explain to you further because Thracian's answer is the solution and I'm looking for something similar to that but better (which I have stated numerous times). ***You are basically telling me, don't use 2 state flows, is that what you are saying?*** – Slodin Nov 29 '22 at 07:46
  • No, it shouldn’t be a problem to even have 100 state flows. Composable documentation says not to create side effects because your Composable function can be called many times without the state changing. If there are no side effects going on, I don’t understand why it’s a problem for your code to be called multiple times when the other unrelated StateFlow changes. – Tenfour04 Nov 29 '22 at 12:36

1 Answers1

0

Simplest thing you can do is providing an Idle or DoNothing state for your UiState which and set to this state when you are done doing event or when you start a new api call. This is a pattern i use a lot, i also use this if there shouldn't be a Loading in ui initially. uiState starts with Idle, when user clicks to fetch from api i set to Loading and then Error or Success based on data fetch result.

I also use this pattern with gestures to not leave state in Up state when user lifts fingers to not start drawing from Up state as in this answer

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • yeah, I have thought about this initially but decided to ask here if there were better solutions. Because always setting back to an Empty (or idle) state seems to be very easy to introduce bugs. Especially working in a team environment, other devs might not remember to do so. – Slodin Nov 27 '22 at 10:16
  • After some days, seems like nobody has a better solution than to do a reset/clear. Thus selecting this as the answer. – Slodin Dec 13 '22 at 22:58