Skip to content

Instantly share code, notes, and snippets.

@hakanai
Last active March 16, 2024 23:43
Show Gist options
  • Save hakanai/f77434402b472f29f2efc857bd04a979 to your computer and use it in GitHub Desktop.
Save hakanai/f77434402b472f29f2efc857bd04a979 to your computer and use it in GitHub Desktop.
Attempt at replacing the Compose error handling to get a better looking dialog

As far as I can tell, replacing the exception handler using CompositionLocalProvider does not work.

More specifically, the exception handler set as the provider is never called with the thrown exception. This can be trivially witnessed by adding a breakpoint in the handler and running the debugger. The breakpoint will never trigger.

Basic unit tests included. Further unit tests might also test what happens when the exception is thrown from various different contexts.

Relevant tickets

import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.window.DialogWindow
@Composable
internal fun BetterErrorDialog(state: ErrorDialogState) {
if (state.isVisible) {
DialogWindow(onCloseRequest = { state.dismissError() }) {
Surface(modifier = Modifier.testTag("ErrorDialog")) {
Text(text = "*** YOU HAVE AN ERROR ***")
}
}
}
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.LocalWindowExceptionHandlerFactory
import androidx.compose.ui.window.WindowExceptionHandler
import androidx.compose.ui.window.WindowExceptionHandlerFactory
import java.awt.Window
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun BetterErrorHandling(content: @Composable () -> Unit) {
val errorDialogState = remember { ErrorDialogState() }
CompositionLocalProvider(LocalWindowExceptionHandlerFactory provides CustomWindowExceptionHandlerFactory(errorDialogState)) {
content()
}
BetterErrorDialog(errorDialogState)
}
@OptIn(ExperimentalComposeUiApi::class)
internal class CustomWindowExceptionHandlerFactory(private val errorDialogState: ErrorDialogState) : WindowExceptionHandlerFactory {
override fun exceptionHandler(window: Window): WindowExceptionHandler {
return CustomWindowExceptionHandler(errorDialogState)
}
}
@OptIn(ExperimentalComposeUiApi::class)
internal class CustomWindowExceptionHandler(private val errorDialogState: ErrorDialogState) : WindowExceptionHandler {
override fun onException(throwable: Throwable) {
errorDialogState.showError(error = throwable)
}
}
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.window.Window
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.AfterEach
class BetterErrorHandlingTest {
// XXX: Forced to use JUnit 4 for tests here because Compose only provides JUnit 4 support
@get:Rule
val compose = createComposeRule()
@Test
fun `error dialog is not shown if nothing bad is happening`() {
compose.setContent {
BetterErrorHandling {
TextButton(modifier = Modifier.testTag("TestButton"), onClick = {}) {
Text(text = "Button")
}
}
}
compose.onNodeWithTag("ErrorDialog").assertDoesNotExist()
}
@Test
fun `error thrown from coroutine causes error dialog to appear`() {
compose.setContent {
BetterErrorHandling {
val scope = rememberCoroutineScope()
TextButton(modifier = Modifier.testTag("TestButton"), onClick = {
scope.launch {
throw SyntheticException()
}
}) {
Text(text = "Button")
}
}
}
compose.onNodeWithTag("TestButton").performClick()
compose.waitForIdle()
compose.onNodeWithTag("ErrorDialog").assertExists()
}
}
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
/**
* State object holding the state of the error dialog.
*/
internal class ErrorDialogState {
internal data class ErrorInfo(val cause: Throwable)
var errorInfo by mutableStateOf(null as ErrorInfo?)
val isVisible by derivedStateOf { errorInfo != null }
fun showError(error: Throwable) {
errorInfo = ErrorInfo(cause = error)
}
fun dismissError() {
errorInfo = null
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment