Hi! The Jetpack Compose documentation insists quit...
# compose
c
Hi! The Jetpack Compose documentation insists quite a lot that
@Composable
functions should not block nor suspend, and says that an event (a button click, for example) should be transmitted ‘up' and dealt with there. I don't really understand what this means or how it works in practice, and I can't find a page that describes that... Assuming I have a simple function (that simulates a network call):
Copy code
suspend fun networkGet(id: Int): String {
  delay(2000)
  return "some interesting value"
}
I would want to have a UI that has a button to launch the request, which then displays the results (whenever they arrive). I guess the
Composable
function should look like this:
Copy code
@Composable
fun SomeView(results: List<String>, onRequest: (Int) -> Unit) {
  Column {
    Button(onClick = { onRequest(5) } {
      Text("Load next")
    }
    for (result in results) {
      Text(result)
    }
  }
}
So far it makes sense to me, but I don't understand how the call site would handle the event.
d
This would not compile, suspend functions have to be called inside a coroutine or another suspending function, even if it would compile it would still throw an exception because you are doing a network operation on the main thread. If you are doing a complete Compose Application you can use compose navigation and tie a viewmodel to your Composetree and access it from every Composable down the tree. With this viewmodel you can implement a function in your viewmodel which gets called in your onClick. The function then starts a coroutine in the viewmodelscope. This is just an example how you could handle this transition upwards. The result then changes the state of a Composable somewhere up your Composable tree. This change of the state triggers a recomposition of the stateful Composable, which create the Composable you are seeing with new Data.
💯 1
👆 1
t
For simple things you can also do it directly in your composable. I wrote a helper composable which returns the progress as state:
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> = { LoadingState.Start },
                             loadingBlock: suspend CoroutineScope.() -> T): LoadingState<T> {
    var state by remember(*inputs) { mutableStateOf(initBlock()) }
    if (state !is LoadingState.Success) {
        LaunchedEffect(*inputs) {
            val loadingSpinnerDelay = async {
                delay(300)
                state = LoadingState.Loading
            }
            state = try {
                LoadingState.Success(loadingBlock())
            } catch (err: Exception) {
                LoadingState.Error(err)
            } finally {
                loadingSpinnerDelay.cancelAndJoin()
            }
        }
    }
    return state
}
Usage:
Copy code
var retryCounter by remember { mutableStateOf(0) }
    val imageState = loadingStateFor(url, retryCounter) {
        //execute your suspend function here
    }
    Crossfade(imageState) { state ->
        when (state) {
            is LoadingState.Start -> Box(
                Modifier
                    .fillMaxSize()
                    .background(backgroundColor))
            is LoadingState.Loading -> LoadingBox(
                Modifier
                    .fillMaxSize()
                    .background(backgroundColor))
            is LoadingState.Success -> Image(bitmap = state.data, contentDescription = "Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
            is LoadingState.Error -> ErrorBox(
                Modifier
                    .fillMaxSize()
                    .background(backgroundColor),
                message = state.error.message, onRetry = { retryCounter++ })
        }
    }
c
Wow, that's a lot of boilerplate just to call a suspend function.
t
the loadingStateFor function is general so you can use it everywhere. The other code is optimized for my usecase. But at the end you need this code. And it does not matter if you use view model or not. Because Compose is declerative and you have to change the UI components depending on the state.
Ok i reduced the a code a littlebit. Maybe this is engough for you:
Copy code
sealed class LoadingState<out T: Any> {
    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?,
                             loadingBlock: suspend CoroutineScope.() -> T): LoadingState<T> {
    var state by remember(*inputs) { mutableStateOf<LoadingState<T>>(LoadingState.Loading) }
    LaunchedEffect(*inputs) {
        state = try {
            LoadingState.Success(loadingBlock())
        } catch (err: Exception) {
            LoadingState.Error(err)
        }
    }
    return state
}


data class CustomData(val test: String)
suspend fun dummyLoader() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
    delay(1000)
    CustomData("Loaded data")
}
@Composable
fun exampleUsage() {
    var retryCounter by remember { mutableStateOf(0) }
    val imageState = loadingStateFor(retryCounter) {
        dummyLoader()
    }
    when (val state = imageState) {
        is LoadingState.Loading -> LoadingBox()
        is LoadingState.Success -> YourDataScreen(state.data)
        is LoadingState.Error -> ErrorBox(message = state.error.message, onRetry = { retryCounter++ })
    }
}
@Composable
fun YourDataScreen(data: CustomData) {
    //....
}
Mybe you could simpliefy it more. If you do not support retry when data loading failed. And maybe you do not want to show an error message.
For my use case i also wanted to check if there is an image already in cache so i needed the init block. And also i do not want to show a loading spinner when the image loading is faster than 300ms.
If you want to do it differently you can just use LaunchedEffect to execute suspend function. But you need to specify a key. When it changes the LaunchedEffect is executed again. The corotuine scope inside of LaunchedEffect is bound to the lifecycle of the composable.
c
That makes a lot of sense, thanks a lot ^^