https://kotlinlang.org logo
#compose
Title
# compose
g

grandstaish

05/17/2020, 1:23 AM
What's the plan regarding try/catch with compose? I really want my parent components to be able to catch and handle certain exceptions from children
a

Adam Powell

05/17/2020, 2:17 AM
Resuming such that try/catch behaves in the usual imperative expected way may or may not be particularly practical, especially because error display and recovery with exception handling is kind of imperative/stateful to begin with
we've had some discussions around how some error recovery constructs might work at various layers; often if something goes wrong you may want to present some alternate UI at some different layer until the condition that triggered the error passes. You don't generally want to re-run the thing that went wrong by default whenever the recomposition would normally want to.
if you have some pseudocode of what you'd like to be able to write, please post it!
It'd probably be worth going over some of the practical recovery considerations that fall out of it
t

Timo Drick

05/17/2020, 8:34 AM
Often you want to download something from you server and than show the result or an error message maybe also show some kind of loading progress. For this i use following code snipped. The result of loadingStateFor can be used in your child component to show progress. In general you would use a when() statement to decide what UI element to show.
Copy code
sealed class LoadingState<out T: Any> {
    object Start: LoadingState<Nothing>()
    object Loading : LoadingState<Nothing>()
    class Error(val error: Throwable): LoadingState<Nothing>()
    class Success<T: Any>(val data: T): LoadingState<T>()
}
@Composable
fun <T: Any> loadingStateFor(vararg inputs: Any?, initBlock: () -> LoadingState<T>, disposeBlock: () -> Unit = {}, loadingBlock: suspend CoroutineScope.() -> T): LoadingState<T> {
    var state by stateFor(*inputs, init = initBlock)

    onCommit(*inputs) {
        val job = if (state !is LoadingState.Success) {
            CoroutineScope(Dispatchers.Main.immediate).launch {
                val loadingSpinnerDelay = async {
                    delay(500)
                    state = LoadingState.Loading
                }
                state = try {
                    LoadingState.Success(loadingBlock())
                } catch (err: Throwable) {
                    LoadingState.Error(err)
                } finally {
                    loadingSpinnerDelay.cancelAndJoin()
                }
            }
        } else null
        onDispose {
            job?.cancel()
            disposeBlock()
        }
    }
    return state
}
g

grandstaish

05/17/2020, 10:54 AM
That all makes sense Adam, I hadn’t really thought it through completely! I’ll explain what I was thinking about writing anyway. Any network request in my app may return with an error that should result in the user getting logged out. Rather than make every service have this potential error as a part of its result API, I thought it would be nice if this just became an exception I could throw. Then one of my top-level composables could catch that error and adjust its state such that the app would start logging out, e.g:
Copy code
@Composable
fun App() {
  val (state, setState) = ...
  when (state) {
    LOGGED_OUT -> { 
      LoginFlow(...)
    }
    LOGGING_OUT -> {
      LoggingOut(onComplete = {
        setState(LOGGED_OUT)
      })
    }
    LOGGED_IN -> {
      try {
        UserFlow(...)
      } catch (ex: MyException) {
        setState(LOGGING_OUT)
      }
    }
  }
}
a

Adam Powell

05/17/2020, 12:49 PM
Yes, exactly - you need some extra machinery around that resulting state transition to make sure you don't just end up in a recomposition loop, plus if it's an internal restart of some implementation detail inside
UserFlow
that triggers the exception, how and what do we restart about the callers when those callers don't have a stack frame anywhere? They completed running long ago. Control flow isn't in the try block anymore.
👍 1
g

grandstaish

05/17/2020, 1:24 PM
Yeah I have no idea how that would work! Thanks for the explanation
t

Timo Drick

05/17/2020, 8:00 PM
I think you should use some kind of business logic layer in your app. And do not do such things on the UI layer. This businesslayer component could than expose the state to the UI using e.g. @Model classes.
11 Views