How do I capture an error in ktor and display it i...
# ktor
s
How do I capture an error in ktor and display it in compose? Attempt (
try
works
catch
doesn't display errors; I've confirmed through debugger that it sets errors in my state flow):
Copy code
[click reply to see code sample]
Copy code
data class ProductDetailState(
    val barcode: String = "",
    val product: PatchApiV3ProductBarcode200ResponseAllOfProduct? = null,
)

private val productDetailStateMutableFlow = MutableStateFlow(
    ProductDetailState()
)
val productDetailStateFlow: StateFlow<ProductDetailState> = productDetailStateMutableFlow

@Composable
fun ShowError(error: String) {
    Text(error, color = Color.Red)
}

@Composable
fun ProductScreen(
    globalState: GlobalState,
    upvote: IUpvote?,
    component: ProductComponent,
    modifier: Modifier = Modifier
) {
    val globalGlobalGlobalStateLocal = globalGlobalState.collectAsState()
    val productStateLocalLocal = productDetailStateFlow.collectAsState()

    Scaffold(
        topBar = {
            ExpandableSearchView(
                searchDisplay = globalGlobalGlobalStateLocal.value.barcode ?: "",
                onSearchDisplayChanged = {},
                onSearchDisplayClosed = {},
            )
        },
        modifier = modifier
    ) { innerPadding ->
        Column(Modifier.padding(innerPadding)) {
            if (!globalGlobalGlobalStateLocal.value.lastErrorStr.isNullOrBlank()) {
                ShowError(globalGlobalGlobalStateLocal.value.lastErrorStr!!)
            } else if (!productStateLocalLocal.value.lastErrorStr.isNullOrBlank()) {
                ShowError(productStateLocalLocal.value.lastErrorStr!!)
            } else if ((globalGlobalGlobalStateLocal.value.barcode?.length ?: 0) > 3) {
                val openFoodFactsApi = OpenFoodFactsApi(HttpClient {
                    /*expectSuccess = true
                    HttpResponseValidator {
                        handleResponseExceptionWithRequest { exception, _ /* request */ ->
                            val clientException = exception as? ClientRequestException
                                ?: return@handleResponseExceptionWithRequest
                            val exceptionResponse = clientException.response
                            if (exceptionResponse.status == HttpStatusCode.NotFound) {
                                val exceptionResponseText = exceptionResponse.bodyAsText()
                                productDetailStateMutableFlow.update { it.copy(lastErrorStr = "[404] $exceptionResponseText") }
                            } else {
                                productDetailStateMutableFlow.update {
                                    it.copy(
                                        lastErrorStr = "[${exceptionResponse.status.value}] ${exceptionResponse.bodyAsText()}"
                                    )
                                }
                            }
                        }
                    }*/
                    install(ContentNegotiation) {
                        json(Json {
                            encodeDefaults = true
                            isLenient = true
                            allowSpecialFloatingPointValues = true
                            allowStructuredMapKeys = true
                            prettyPrint = false
                            useArrayPolymorphism = false
                            ignoreUnknownKeys = true
                        })
                    }
                })

                val scope = rememberCoroutineScope()
                LaunchedEffect(globalGlobalGlobalStateLocal.value.barcode) {
                    try {
                        val product = scope.async {
                            openFoodFactsApi.productById(globalGlobalGlobalStateLocal.value.barcode!!)
                        }.await().product
                        if (product?.productName != null) {
                            productDetailStateMutableFlow.update { it.copy(productName = product.productName) }
                        }
                        if (product?.imageFrontUrl != null) {
                            productDetailStateMutableFlow.update { it.copy(imageFrontUrl = product.imageFrontUrl) }
                        }
                    } catch (e: CancellationException) {
                        currentCoroutineContext().ensureActive()
                    } catch (e: HttpResponseException) {
                        productDetailStateMutableFlow.update {
                            it.copy(
                                lastErrorStr = "[${e.response.status}] ${e.response.bodyAsText()}"
                            )
                        }
                    } catch (e: SocketTimeoutException) {
                        productDetailStateMutableFlow.update {
                            it.copy(
                                lastErrorStr = "[SocketTimeoutException] ${e.message}"
                            )
                        }
                    }
                }
                if (!productStateLocalLocal.value.productName.isNullOrEmpty()) {
                    Column {
                        Text(productStateLocalLocal.value.productName)
                        SubcomposeAsyncImage(
                            model = productStateLocalLocal.value.imageFrontUrl,
                            loading = {
                                CircularProgressIndicator()
                            },
                            contentDescription = "${productStateLocalLocal.value.productName} image",
                            onError = { e -> println("img e $e") }
                        )
                    }
                } else if (!globalGlobalGlobalStateLocal.value.lastErrorStr.isNullOrBlank()) {
                    ShowError(globalGlobalGlobalStateLocal.value.lastErrorStr!!)
                } else if (!productStateLocalLocal.value.lastErrorStr.isNullOrBlank()) {
                    ShowError(productStateLocalLocal.value.lastErrorStr!!)
                } else {
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        CircularProgressIndicator(
                            modifier = Modifier.width(64.dp),
                        )
                    }
                }


                Text("globalGlobalGlobalStateLocal.value.lastErrorStr.isNullOrBlank(): ${globalGlobalGlobalStateLocal.value.lastErrorStr.isNullOrBlank()}")
                Text("globalGlobalGlobalStateLocal.value.lastErrorStr = ${globalGlobalGlobalStateLocal.value.lastErrorStr}")
                Text("produceStateLocalLocal.value.lastErrorStr.isNullOrBlank(): ${productStateLocalLocal.value.lastErrorStr.isNullOrBlank()}")
                Text("produceStateLocalLocal.value.lastErrorStr = ${productStateLocalLocal.value.lastErrorStr}")
                /*if (globalGlobalGlobalStateLocal.barcode.isNullOrEmpty()
                    || produceStateLocalLocal.productName.isNullOrEmpty()
                ) {*/
            }
        }
    }
}
s
Brother there is a lot to be said here, not even taking things like
globalGlobalGlobalStateLocal
into account. But there is a strong chance that your error is not displaying as the API doesn't get a decent chance to return a value as it will be rebuilt with every recomposition where the barcode is > 3, or because you are launching a coroutine within the coroutine that contains your try-catch block. I would strongly suggest taking another look at how things like `ViewModel`s,
Repositories
and items like
remember
work to get a better grip on how to deal with architectures & UI-state in Android. Things I would do in short: • Move creation of
HttpClient
&
OpenFoodFactsApi
out of the UI (ideally use something Koin for dependency injection but if you're not familiar with that concept yet, just do it somewhere not in a Composable). • Create a
OpenFoodFactsViewModel
which has a trigger to get
productById
and afterward translate the output from the
OpenFoodFactsApi
into a state-object. (This is where your try-catch could be done so you can figure out whether the state you're returning to the UI is either data or an error) • Collect that state object from
OpenFoodFactsViewModel
in your
ProductScreen
There are plenty of similar samples on developer.android that can help you out: https://developer.android.com/develop/ui/compose/architecture#additional-resources
☝🏼 1
☝️ 1
s
Thanks yeah I know I'm doing a bunch of things wrong just a lot of the ViewModel dependencies don't work when I target all the platforms. MVIKotlin seems to be an alternative, but then it wasn't obvious how that connects with Compose. Anyway I'll read up on https://developer.android.com/topic/architecture/data-layer and the link you shared and try and make headway
👍 1
s
Ah ok it's for a Multiplatform project, if you're looking for MVI I'm personally a very big fan of Ballast: https://copper-leaf.github.io/ballast/wiki/usage/mental-model/ A lot of the "technicalities" of MVI will be taken up by the framework and it enables you to very easily scale up in features. Feel free to take a look at the mental model and for any questions you can always join us on #C03GTEJ9Y3E.
s
Thanks I'll take a look. In the meantime I went to very alpha & dev versions of all the dependencies and now have
ViewModel
working on Android and desktop. Time to try and get it working on iOS and web next.