Get Compose building and running on the JVM

This commit is contained in:
Jake Wharton
2020-09-04 16:57:50 -04:00
parent 01d8b73e26
commit 663db55d2d
9 changed files with 322 additions and 0 deletions

4
.gitmodules vendored
View File

@ -1,3 +1,7 @@
[submodule "yoga/upstream"]
path = yoga/upstream
url = https://github.com/facebook/yoga
[submodule "compose/upstream"]
path = compose/upstream
url = https://android.googlesource.com/platform/frameworks/support
branch = androidx-master-dev

View File

@ -1,8 +1,10 @@
buildscript {
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0'
classpath "dev.nokee.jni-library:dev.nokee.jni-library.gradle.plugin:0.4.0"
}
repositories {
mavenCentral()
gradlePluginPortal()
}
}
@ -10,5 +12,19 @@ buildscript {
subprojects {
repositories {
mavenCentral()
google()
maven {
url 'https://kotlin.bintray.com/kotlinx'
}
}
tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinCompile).configureEach { task ->
task.kotlinOptions {
freeCompilerArgs += [
'-progressive',
'-Xopt-in=kotlin.RequiresOptIn',
'-Xopt-in=androidx.compose.runtime.ExperimentalComposeApi',
]
}
}
}

51
compose/build.gradle Normal file
View File

@ -0,0 +1,51 @@
apply plugin: 'org.jetbrains.kotlin.multiplatform'
kotlin {
jvm()
sourceSets {
commonMain {
kotlin {
srcDir 'upstream/compose/runtime/runtime/src/commonMain/kotlin'
srcDir 'upstream/compose/runtime/runtime-dispatch/src/commonMain/kotlin'
}
dependencies {
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
api 'org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.3'
}
}
jvmMain {
kotlin {
srcDir 'upstream/compose/runtime/runtime/src/jvmMain/kotlin'
}
}
}
}
configurations {
kotlinPlugin
}
dependencies {
kotlinPlugin 'androidx.compose:compose-compiler:1.0.0-alpha02'
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
useIR = true
}
def pluginConfiguration = configurations.kotlinPlugin
dependsOn(pluginConfiguration)
doFirst {
if (!file('upstream/.git').exists()) {
throw new RuntimeException(
"Missing 'upstream' git submodule clone. Did you run 'git submodule update --init'?")
}
if (!pluginConfiguration.isEmpty()) {
kotlinOptions.freeCompilerArgs +=
"-Xplugin=${pluginConfiguration.files.first()}"
}
}
}

View File

@ -0,0 +1,67 @@
package androidx.compose.runtime
// TODO The notion of a global EmbeddingContext actual should not exist! It breaks concurrent
// usage on multiple threads which target multiple threads. This should always be pulled from the
// Recomposer.
var yoloGlobalEmbeddingContext: EmbeddingContext? = null
actual fun EmbeddingContext(): EmbeddingContext = yoloGlobalEmbeddingContext!!
private val keyInfo = mutableMapOf<Int, String>()
private fun findSourceKey(key: Any): Int? =
when (key) {
is Int -> key
is JoinedKey -> {
key.left?.let { findSourceKey(it) } ?: key.right?.let { findSourceKey(it) }
}
else -> null
}
internal actual fun recordSourceKeyInfo(key: Any) {
val sk = findSourceKey(key)
sk?.let {
keyInfo.getOrPut(sk, {
val stack = Thread.currentThread().stackTrace
// On Android the frames looks like:
// 0: getThreadStackTrace() (native method)
// 1: getStackTrace()
// 2: recordSourceKey()
// 3: start()
// 4: startGroup() or startNode()
// 5: non-inline call/emit?
// 5 or 6: <calling method>
// On a desktop VM this looks like:
// 0: getStackTrace()
// 1: recordSourceKey()
// 2: start()
// 3: startGroup() or startNode()
// 4: non-inline call/emit?
// 4 or 5: <calling method>
// If the stack method at 4 is startGroup assume we want 5 instead.
val frameNumber = stack[4].let {
if (it.methodName == "startGroup" || it.methodName == "startNode") 5 else 4
}
val frame = stack[frameNumber].let {
if (it.methodName == "call" || it.methodName == "emit")
stack[frameNumber + 1]
else
stack[frameNumber]
}
"${frame.className}.${frame.methodName} (${frame.fileName}:${frame.lineNumber})"
})
}
}
actual fun keySourceInfoOf(key: Any): String? = keyInfo[key]
actual fun resetSourceInfo(): Unit = keyInfo.clear()
internal actual object Trace {
actual fun beginSection(name: String) {
}
actual fun endSection() {
}
}
actual annotation class MainThread
actual annotation class CheckResult(actual val suggest: String)

View File

@ -0,0 +1,6 @@
package androidx.compose.runtime.dispatch
actual val DefaultMonotonicFrameClock: MonotonicFrameClock get() {
throw UnsupportedOperationException(
"No default MonotonicFrameClock! You must include one in your CoroutineContext.")
}

1
compose/upstream Submodule

Submodule compose/upstream added at 31022a2dda

26
mosaic/build.gradle Normal file
View File

@ -0,0 +1,26 @@
apply plugin: 'org.jetbrains.kotlin.jvm'
configurations {
kotlinPlugin
}
dependencies {
implementation project(':compose')
kotlinPlugin 'androidx.compose:compose-compiler:1.0.0-alpha02'
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
useIR = true
}
def pluginConfiguration = configurations.kotlinPlugin
dependsOn(pluginConfiguration)
doFirst {
if (!pluginConfiguration.isEmpty()) {
kotlinOptions.freeCompilerArgs +=
"-Xplugin=${pluginConfiguration.files.first()}"
}
}
}

View File

@ -0,0 +1,149 @@
package example
import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.EmbeddingContext
import androidx.compose.runtime.FrameManager
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionFor
import androidx.compose.runtime.dispatch.BroadcastFrameClock
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.runtime.emit
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.withMutableSnapshot
import androidx.compose.runtime.yoloGlobalEmbeddingContext
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.RENDEZVOUS
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
sealed class Node {
abstract val children: MutableList<Node>
}
class Line : Node() {
var value: String = ""
override fun toString() = value
override val children: MutableList<Node>
get() = throw UnsupportedOperationException()
}
class Lines : Node() {
override val children = mutableListOf<Node>()
override fun toString() = buildString {
children.forEachIndexed { index, node ->
if (index > 0) {
append('\n')
}
append(node)
}
}
}
class NodeApplier(root: Node) : AbstractApplier<Node>(root) {
override fun insert(index: Int, instance: Node) {
root.children.add(index, instance)
}
override fun remove(index: Int, count: Int) {
root.children.remove(index, count)
}
override fun move(from: Int, to: Int, count: Int) {
root.children.move(from, to, count)
}
override fun onClear() {
}
}
fun main() {
runBlocking {
val mainThread = Thread.currentThread()
val clock = BroadcastFrameClock()
val composeContext = coroutineContext + clock
val embeddingContext = object : EmbeddingContext {
override fun isMainThread(): Boolean {
return Thread.currentThread() === mainThread
}
override fun mainThreadCompositionContext(): CoroutineContext {
return composeContext
}
}
yoloGlobalEmbeddingContext = embeddingContext
val recomposer = Recomposer(embeddingContext)
val lines = Lines()
val applier = NodeApplier(lines)
val composition = compositionFor(Any(), applier, recomposer)
composeContext[Job]!!.invokeOnCompletion {
composition.dispose()
}
withContext(composeContext) {
launch(start = UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
val counts = mutableStateOf(0)
composition.setContent {
val count by remember { counts }
Line("The count is $count")
}
launch(start = UNDISPATCHED) {
while (true) {
clock.sendFrame(System.nanoTime())
println(lines)
delay(500)
}
}
for (i in 1..10) {
delay(1_000)
withMutableSnapshot {
counts.value = i
}
}
// Wait for final frame.
clock.withFrameNanos { }
cancel()
}
}
}
@Composable
fun Line(value: String) {
emit<Line, NodeApplier>(::Line) {
set(value) {
this.value = value
}
}
}

View File

@ -1,5 +1,7 @@
rootProject.name = 'mosaic'
include ':compose'
include ':mosaic'
include ':yoga'
include ':yoga:core'
include ':yoga:java'