diff --git a/README.md b/README.md index 63967760..2eba071d 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Check the [Developers Guide](/component-toolkit/README.md) in the component-tool **Artifacts are published on Maven Central:** ```kotlin -val commonMain by getting { - dependencies { +sourceSets { + commonMain.dependencies { // The basic components and plugins API. Use it if you have an // application architecture already and you just want to use some plugin implementations. implementation("io.github.pablichjenkov:component-toolkit:0.6.1") diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index fa0ae23e..e8ddb473 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -20,9 +20,9 @@ kotlin { implementation(project(":macao-sdk-di-koin")) implementation(project(":macao-sdk-di-manual")) implementation(compose.material3) - implementation("androidx.activity:activity-compose:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("androidx.activity:activity-compose:1.8.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") } } } diff --git a/component-toolkit/build.gradle.kts b/component-toolkit/build.gradle.kts index cf415632..f864a36f 100644 --- a/component-toolkit/build.gradle.kts +++ b/component-toolkit/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { kotlin("multiplatform") id("com.android.library") @@ -138,17 +140,17 @@ kotlin { } } - // JS + // Browser js(IR) { browser() } - // WASM, once kotlin 1.8.20 is out. Although I believe this should go in the jsApp module not - // in the library. Perhaps can go here without the binaries.executable() statement - /*wasm { - binaries.executable() - browser {} - }*/ + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "ComponentToolkitKt" + browser() + binaries.library() + } // JVM jvm() @@ -161,7 +163,7 @@ kotlin { implementation(compose.ui) implementation(compose.material3) implementation(compose.animation) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // implementation("org.jetbrains.compose.ui:ui-util:1.5.10") // implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") } diff --git a/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/component/BrowserComponentRender.kt b/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/component/BrowserComponentRender.kt new file mode 100644 index 00000000..96fdbc63 --- /dev/null +++ b/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/component/BrowserComponentRender.kt @@ -0,0 +1,49 @@ +package com.macaosoftware.component + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.macaosoftware.component.core.Component +import com.macaosoftware.component.core.deeplink.LocalRootComponentProvider +import com.macaosoftware.plugin.Lifecycle +import com.macaosoftware.plugin.LifecycleEventObserver + +@Composable +fun BrowserComponentRender( + rootComponent: Component +) { + + val lifecycle = remember(rootComponent) { Lifecycle() } + + CompositionLocalProvider( + LocalRootComponentProvider provides rootComponent + ) { + rootComponent.Content(Modifier.fillMaxSize()) + /*Box(modifier = Modifier.fillMaxSize()) { + // Should listen for keyboard back instead + FloatingBackButton( + modifier = Modifier.offset(y = 48.dp), + alignment = Alignment.TopStart, + onClick = { webBackPressDispatcher.dispatchBackPressed() } + ) + }*/ + } + + LifecycleEventObserver( + lifecycle = lifecycle, + onStart = { + println("Receiving Js.onStart() event") + rootComponent.dispatchActive() + }, + onStop = { + println("Receiving Js.onStop() event") + rootComponent.dispatchInactive() + }, + initializeBlock = { + rootComponent.dispatchAttach() + } + ) + +} diff --git a/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/plugin/Lifecycle.kt b/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/plugin/Lifecycle.kt new file mode 100644 index 00000000..0d61feac --- /dev/null +++ b/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/plugin/Lifecycle.kt @@ -0,0 +1,30 @@ +package com.macaosoftware.plugin + +class Lifecycle { + + private val platformLifecyclePlugin = DefaultPlatformLifecyclePlugin() + + init { + startObserving() + } + + private fun startObserving() { + started() + } + + fun subscribe(appLifecycleCallback: AppLifecycleCallback) { + platformLifecyclePlugin.subscribe(appLifecycleCallback) + } + + private fun started() { + platformLifecyclePlugin.dispatchAppLifecycleEvent(AppLifecycleEvent.Start) + } + + private fun stopped() { + platformLifecyclePlugin.dispatchAppLifecycleEvent(AppLifecycleEvent.Stop) + } + + fun unsubscribe(appLifecycleCallback: AppLifecycleCallback) { + platformLifecyclePlugin.unsubscribe(appLifecycleCallback) + } +} diff --git a/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/plugin/LifecycleEventObserver.kt b/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/plugin/LifecycleEventObserver.kt new file mode 100644 index 00000000..b5ce71b3 --- /dev/null +++ b/component-toolkit/src/wasmJsMain/kotlin/com/macaosoftware/plugin/LifecycleEventObserver.kt @@ -0,0 +1,46 @@ +package com.macaosoftware.plugin + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState + +@Composable +fun LifecycleEventObserver( + lifecycle: Lifecycle, + onStart: () -> Unit, + onStop: () -> Unit, + initializeBlock: () -> Unit +) { + // Safely update the current lambdas when a new one is provided + val currentOnStart by rememberUpdatedState(newValue = onStart) + val currentOnStop by rememberUpdatedState(newValue = onStop) + + // If the enclosing lifecycleOwner changes, dispose and reset the effect + DisposableEffect(key1 = initializeBlock, key2 = lifecycle) { + initializeBlock.invoke() + // Create an observer that triggers our remembered callbacks + // when the LifecycleOwner that contains this composable changes its state. + val observer = object : AppLifecycleCallback { + override fun onEvent(appLifecycleEvent: AppLifecycleEvent) { + if (appLifecycleEvent == AppLifecycleEvent.Start) { + currentOnStart() + } else if (appLifecycleEvent == AppLifecycleEvent.Stop) { + currentOnStop() + } + } + } + + // Add the observer to the lifecycle + lifecycle.subscribe(observer) + + // When the effect leaves the Composition, remove the observer + onDispose { + lifecycle.unsubscribe(observer) + println( + "LifecycleEventObserver::Disposing LifecycleEventObserver" + ) + } + } + +} diff --git a/gradle.properties b/gradle.properties index 73a70f1c..7f756550 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,8 +10,8 @@ kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.androidSourceSetLayoutVersion=2 # Compose -org.jetbrains.compose.experimental.uikit.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true # Android agp.version=8.3.1 @@ -30,4 +30,4 @@ kotlin.version=1.9.23 compose.version=1.6.1 # component-toolkit version -component-toolkit.version=0.6.1 +component-toolkit.version=0.6.2 diff --git a/jsApp/build.gradle.kts b/jsApp/build.gradle.kts index 2cf43aca..c1db4e27 100644 --- a/jsApp/build.gradle.kts +++ b/jsApp/build.gradle.kts @@ -31,8 +31,8 @@ kotlin { implementation(project(":shared")) implementation(project(":macao-sdk-di-koin")) implementation(project(":macao-sdk-di-manual")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.8.0") } } } diff --git a/jvmApp/build.gradle.kts b/jvmApp/build.gradle.kts index d5b20e3b..5a05ff2a 100644 --- a/jvmApp/build.gradle.kts +++ b/jvmApp/build.gradle.kts @@ -13,8 +13,8 @@ kotlin { implementation(compose.desktop.common) implementation(compose.desktop.currentOs) implementation(compose.material3) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") } } } diff --git a/macao-sdk-di-koin/build.gradle.kts b/macao-sdk-di-koin/build.gradle.kts index 38b27115..34eb1c99 100644 --- a/macao-sdk-di-koin/build.gradle.kts +++ b/macao-sdk-di-koin/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { kotlin("multiplatform") id("com.android.library") @@ -111,6 +113,13 @@ kotlin { browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "MacaoSdkKoin" + browser() + binaries.library() + } + jvm() sourceSets { @@ -118,25 +127,26 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.ui) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // Macao Libs api(project(":component-toolkit")) // Koin - api("io.insert-koin:koin-core:3.5.3") + // api("io.insert-koin:koin-core:3.5.3") + api("io.insert-koin:koin-core:3.6.0-wasm-alpha2") } commonTest.dependencies { // implementation(libs.kotlin.test) } androidMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") } jvmMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") } jsMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.8.0") } } } diff --git a/macao-sdk-di-koin/src/wasmJsMain/kotlin/com/macaosoftware/app/MacaoKoinApplication.kt b/macao-sdk-di-koin/src/wasmJsMain/kotlin/com/macaosoftware/app/MacaoKoinApplication.kt new file mode 100644 index 00000000..7d895216 --- /dev/null +++ b/macao-sdk-di-koin/src/wasmJsMain/kotlin/com/macaosoftware/app/MacaoKoinApplication.kt @@ -0,0 +1,25 @@ +package com.macaosoftware.app + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import com.macaosoftware.component.BrowserComponentRender + +@Composable +fun MacaoKoinApplication( + applicationState: MacaoKoinApplicationState +) { + + when (val stage = applicationState.stage.value) { + + KoinAppStage.Created -> { + SideEffect { applicationState.start() } + } + + KoinAppStage.Loading -> { + } + + is KoinAppStage.Started -> { + BrowserComponentRender(rootComponent = stage.rootComponent) + } + } +} diff --git a/macao-sdk-di-manual/build.gradle.kts b/macao-sdk-di-manual/build.gradle.kts index 844faef6..708d58f7 100644 --- a/macao-sdk-di-manual/build.gradle.kts +++ b/macao-sdk-di-manual/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { kotlin("multiplatform") id("com.android.library") @@ -25,6 +27,13 @@ kotlin { browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "MacaoSdkKoin" + browser() + binaries.library() + } + jvm() sourceSets { @@ -32,7 +41,7 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.ui) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // Macao Libs api(project(":component-toolkit")) @@ -41,13 +50,13 @@ kotlin { // implementation(libs.kotlin.test) } androidMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") } jvmMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") } jsMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.8.0") } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f1ed80c..735de5a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ include(":shared") include(":androidApp") include(":jsApp") include(":jvmApp") +include(":wasmApp") pluginManagement { repositories { diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 0a68f994..a7537d1b 100755 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { kotlin("multiplatform") id("org.jetbrains.compose") @@ -34,6 +36,13 @@ kotlin { browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "shared" + browser() + binaries.library() + } + // JVM jvm() @@ -43,7 +52,7 @@ kotlin { implementation(compose.ui) implementation(compose.foundation) implementation(compose.material3) - implementation("org.jetbrains.compose.components:components-resources:1.5.11") + implementation("org.jetbrains.compose.components:components-resources:1.6.1") // Macao Libs api(project(":macao-sdk-di-koin")) diff --git a/wasmApp/build.gradle.kts b/wasmApp/build.gradle.kts new file mode 100644 index 00000000..39efbca1 --- /dev/null +++ b/wasmApp/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +compose { + // Sets a specific JetBrains Compose Compiler version + /* + kotlinCompilerPlugin.set("1.4.7") + // Sets a specific Google Compose Compiler version + // kotlinCompilerPlugin.set("androidx.compose.compiler:compiler:1.4.2") + kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=1.8.21") + */ + + experimental { + web.application {} + } +} + +kotlin { + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "wasmApp" + browser { + commonWebpackConfig { + outputFileName = "wasmApp.js" + devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { + static = (static ?: mutableListOf()).apply { + // Serve sources to debug inside browser + add(project.projectDir.path) + add(project.projectDir.path + "/commonMain/") + add(project.projectDir.path + "/wasmJsMain/") + } + } + } + } + binaries.executable() + } + + sourceSets { + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.ui) + implementation(compose.material3) + implementation(project(":shared")) + implementation(project(":macao-sdk-di-koin")) + implementation(project(":macao-sdk-di-manual")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } + +} diff --git a/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/BrowserRootComponentKoinProvider.kt b/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/BrowserRootComponentKoinProvider.kt new file mode 100644 index 00000000..49405454 --- /dev/null +++ b/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/BrowserRootComponentKoinProvider.kt @@ -0,0 +1,24 @@ +package com.macaosoftware.component.demo + +import com.macaosoftware.app.RootComponentKoinProvider +import com.macaosoftware.component.core.Component +import com.macaosoftware.component.demo.viewmodel.StackDemoViewModel +import com.macaosoftware.component.demo.viewmodel.factory.StackDemoViewModelFactory +import com.macaosoftware.component.stack.StackComponent +import com.macaosoftware.component.stack.StackComponentDefaults +import org.koin.core.component.KoinComponent + +class BrowserRootComponentKoinProvider : RootComponentKoinProvider { + + override suspend fun provideRootComponent(koinComponent: KoinComponent): Component { + return StackComponent( + viewModelFactory = StackDemoViewModelFactory( + stackStatePresenter = StackComponentDefaults.createStackStatePresenter(), + onBackPress = { true } + ), + content = StackComponentDefaults.DefaultStackComponentView + ).also { + it.deepLinkPathSegment = "_root_navigator_stack" + } + } +} \ No newline at end of file diff --git a/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/BrowserRootComponentProvider.kt b/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/BrowserRootComponentProvider.kt new file mode 100644 index 00000000..ad43402e --- /dev/null +++ b/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/BrowserRootComponentProvider.kt @@ -0,0 +1,28 @@ +package com.macaosoftware.component.demo + +import com.macaosoftware.app.PluginManager +import com.macaosoftware.app.RootComponentProvider +import com.macaosoftware.component.core.Component +import com.macaosoftware.component.demo.viewmodel.StackDemoViewModel +import com.macaosoftware.component.demo.viewmodel.factory.StackDemoViewModelFactory +import com.macaosoftware.component.stack.StackComponent +import com.macaosoftware.component.stack.StackComponentDefaults +import kotlinx.coroutines.delay + +class BrowserRootComponentProvider : RootComponentProvider { + + override suspend fun provideRootComponent( + pluginManager: PluginManager + ): Component { + + return StackComponent( + viewModelFactory = StackDemoViewModelFactory( + stackStatePresenter = StackComponentDefaults.createStackStatePresenter(), + onBackPress = { true } + ), + content = StackComponentDefaults.DefaultStackComponentView + ).also { + it.deepLinkPathSegment = "_root_navigator_stack" + } + } +} \ No newline at end of file diff --git a/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/Main.kt b/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/Main.kt new file mode 100644 index 00000000..07135c7f --- /dev/null +++ b/wasmApp/src/wasmJsMain/kotlin/com/macaosoftware/component/demo/Main.kt @@ -0,0 +1,23 @@ +package com.macaosoftware.component.demo + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import com.macaosoftware.app.MacaoKoinApplication +import com.macaosoftware.app.MacaoKoinApplicationState +import com.macaosoftware.component.demo.plugin.DemoKoinRootModuleInitializer +import kotlinx.coroutines.Dispatchers + +fun main() { + val applicationState = MacaoKoinApplicationState( + dispatcher = Dispatchers.Default, + rootComponentKoinProvider = BrowserRootComponentKoinProvider(), + koinRootModuleInitializer = DemoKoinRootModuleInitializer() + ) + + CanvasBasedWindow( + title = "Macao SDK Demo", + canvasElementId = "ComposeTarget" + ) { + MacaoKoinApplication(applicationState = applicationState) + } +} diff --git a/wasmApp/src/wasmJsMain/resources/index.html b/wasmApp/src/wasmJsMain/resources/index.html new file mode 100644 index 00000000..2013bdcb --- /dev/null +++ b/wasmApp/src/wasmJsMain/resources/index.html @@ -0,0 +1,12 @@ + + + + + + + Component Toolkit + + + + +