Li Zhenxin
Li Zhenxin

Reputation: 363

How to restart Looper when Exception throwed in Jetpack Compose?

I wan`t to make a global crash handler, so I extend Thread.UncaughtExceptionHandler and set this as default uncaught exception handler. I restart the main thread looper in this handler:

class JavaUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
    init {
        Thread.setDefaultUncaughtExceptionHandler(this)
    }


    override fun uncaughtException(t: Thread, e: Throwable) {
        saveCrashMessageToLocal(t, e)
        handleException(t, e)
        restartLooper(t)
    }

    private fun restartLooper(thread: Thread) {
        // restart looper when this thread is main thread
        if (thread == Looper.getMainLooper().thread) {
            while (true) {
                try {
                    Looper.loop()
                } catch (e: Throwable) {
                    handleException(Thread.currentThread(), e)
                }
            }
        }
    }
}

It works as I expect when I use Xml view, such as:

setContentView(R.layout.layout_main)
findViewById<Button>(R.id.crash).setOnClickListener { throwExc() }
findViewById<Button>(R.id.flow_crash).setOnClickListener { throwFlowExe() }

when exception happen, this handler catch it and restart the main looper, and then the app can run as normal.

But when using Compose View and throwing exception, app cannot run normally:

@Composable
fun Greeting(name: String) {
    Button(onClick = { throwExc() }) {
        Text(text = "Hello NewActivity!", modifier = Modifier)
    }

    Button(onClick = { throwFlowExe() }) {
        Text(text = "Hello NewProcessActivity!", modifier = Modifier)
    }
}

I guess the reason is the main looper is not be restarting, but I don`t know the difference when clicking view between Xml View and Compose View.

Upvotes: 3

Views: 581

Answers (1)

Watermelon
Watermelon

Reputation: 632

Short answer impossible.

Jetpack compose is not managed by Looper, it's managed by Composer which is managed by Coroutine and Recomposer. Recomposer manages the animation sync which is handled by MonotonicFrameClock.

onClick is called by pointerInput. which runs gesture inside suspend function. Which is generally managed by LaunchedEffect.

So generally Speaking if you throw exception normally in compose. It will first reach the coroutine context of composer and shutdown the composer (because job is destroyed) and then call UncaughtExceptionHandler.

Just preventing app being crashed when using jetpack compose providing coroutineExceptionHandler to the Recomposer is enough. No need to restart the Looper since the exception is caught by CoroutineExceptionHandler.

        val recomposer = window.decorView.createLifecycleAwareWindowRecomposer(
            CoroutineExceptionHandler { coroutineContext, throwable -> throwable.printStackTrace() }, lifecycle)
        window.decorView.compositionContext = recomposer
        setContent {
                    Button(onClick = {
                        Handler(this.mainLooper).post {
                            // print shutdown
                            println(recomposer.currentState.value)
                        }
                        throw RuntimeException("Throw")
                    }) {
                        Text(text = "AAA")
                    }


        }

So what really matter for your case is how to caught exception before it reaches the root level composition context.

However that is not possible. CompositionContext have two context, effectCoroutineContext where all effects(LaunchedEffect, SideEffect, onClick etc), and recomposeCoroutineContext where recompose runs.

And effectCoroutineContext is always linked to Recomposer's effectiveJob so there is no way to intercept the exception before is reaches Recomposer by magic.

Overidng UIComposable recomposeCoroutineContext is also not possible, because you have create child Composition with UIApplier while UIApplier is internal.

Upvotes: 3

Related Questions