https://kotlinlang.org logo
#compose
Title
# compose
g

gitai

06/22/2021, 5:35 PM
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

06/22/2021, 5:39 PM
g

gitai

06/22/2021, 9:36 PM
Thanks!
m

mattinger

06/23/2021, 12:39 AM
@gitai I was able to pull out the required stuff from there to do my testing into a file that’s < 100 lines long.
Copy code
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

06/23/2021, 12:47 AM
@mattinger where you able to run it from main() ?
m

mattinger

06/23/2021, 12:51 AM
i haven’t tried, but i’m able to run it from junit tests, so i don’t see why not,
g

gitai

06/23/2021, 1:00 AM
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