Thread
#compose
    g

    gitai

    1 year ago
    I added a Kotlin file having main() to an AS Compose project for quick testing. Is there an api to execute a root composable function from a non-composable main() , similar to how we can use
    runBlocking()
    to launch a root coroutine from a non-suspendable main?
    m

    Marko Novakovic

    1 year ago
    g

    gitai

    1 year ago
    Thanks!
    m

    mattinger

    1 year ago
    @gitai I was able to pull out the required stuff from there to do my testing into a file that’s < 100 lines long.
    import androidx.compose.runtime.*
    import androidx.compose.runtime.snapshots.Snapshot
    import androidx.compose.ui.test.TestMonotonicFrameClock
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.ExperimentalCoroutinesApi
    import kotlinx.coroutines.coroutineScope
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.test.runBlockingTest
    import kotlin.coroutines.CoroutineContext
    import kotlin.coroutines.EmptyCoroutineContext
    
    private fun callSetContent(composition: Composition, content: @Composable () -> Unit) {
        composition.setContent(content)
    }
    
    @OptIn(InternalComposeApi::class)
    @Composable
    fun TestSubcomposition(
        content: @Composable () -> Unit
    ) {
        val parentRef = rememberCompositionContext()
        val currentContent by rememberUpdatedState(content)
        DisposableEffect(parentRef) {
            val subcomposition = Composition(EmptyApplier(), parentRef)
            // TODO: work around for b/179701728
            callSetContent(subcomposition) {
                // Note: This is in a lambda invocation to keep the currentContent state read
                // in the subcomposition's content composable. Changing this to be
                // subcomposition.setContent(currentContent) would snapshot read only on initial set.
                currentContent()
            }
            onDispose {
                subcomposition.dispose()
            }
        }
    }
    
    class EmptyApplier : Applier<Unit> {
        override val current: Unit = Unit
        override fun down(node: Unit) {}
        override fun up() {}
        override fun insertTopDown(index: Int, instance: Unit) {
            error("Unexpected")
        }
        override fun insertBottomUp(index: Int, instance: Unit) {
            error("Unexpected")
        }
        override fun remove(index: Int, count: Int) {
            error("Unexpected")
        }
        override fun move(from: Int, to: Int, count: Int) {
            error("Unexpected")
        }
        override fun clear() {}
    }
    
    @OptIn(ExperimentalCoroutinesApi::class)
    suspend fun <R> localRecomposerTest(
        block: CoroutineScope.(Recomposer) -> R
    ) = coroutineScope {
        val contextWithClock = coroutineContext + TestMonotonicFrameClock(this)
        val recomposer = Recomposer(contextWithClock)
        launch(contextWithClock) {
            recomposer.runRecomposeAndApplyChanges()
        }
        block(recomposer)
        // This call doesn't need to be in a finally; everything it does will be torn down
        // in exceptional cases by the coroutineScope failure
        recomposer.cancel()
    }
    
    fun runBlockingCompositionTest(
        context: CoroutineContext = EmptyCoroutineContext,
        composable: @Composable () -> Unit
    ) {
        runBlockingTest(context = context) {
            localRecomposerTest { recomposer ->
                val composition = Composition(EmptyApplier(), recomposer)
    
                Snapshot.notifyObjectsInitialized()
                composition.setContent {
                    TestSubcomposition {
                        composable()
                    }
                }
    
                Snapshot.sendApplyNotifications()
                advanceUntilIdle()
            }
        }
    }
    that last function i wrote myself to eliminate some of the boilerplate.
    g

    gitai

    1 year ago
    @mattinger where you able to run it from main() ?
    m

    mattinger

    1 year ago
    i haven’t tried, but i’m able to run it from junit tests, so i don’t see why not,
    g

    gitai

    1 year ago
    I'm hitting an exception Exception in thread "main" java.lang.RuntimeException: Stub! at android.os.Trace.beginSection(Trace.java:27) at androidx.compose.runtime.Trace.beginSection(ActualAndroid.android.kt:30) at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3451) ...
    I must say its pity there are no simple non ui apis to trace / experiment with recompositions. IMO, Compose introduces a completely new programing paradigm thats orthogonal to UI and I believe its important to invest some time writing "text based apps" (not test code) exploring different state management strategies before diving into the whole UI shebang