arkivanov / Decompose

Kotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing (navigation) and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.)
https://arkivanov.github.io/Decompose
Apache License 2.0
2.15k stars 84 forks source link

Compose for iOS, macOS and Web #74

Closed arkivanov closed 9 months ago

arkivanov commented 2 years ago

Latest builds of JetBrains Compose support native iOS and macOS targets. Let's try to add it to extensions-compose-jetbrains module. Since it's far from stable, a separate branch seems reasonable.

Status update: experimental -native-compose versions of Decompose with Compose for Darwin (Apple/iOS) are published from the compose-experimental branch, and have <version>-compose-experimental version suffix.

arkivanov commented 2 years ago

Created the branch: compose-darwin.

arkivanov commented 2 years ago

The compiler crashes on CI, filed: https://github.com/JetBrains/compose-jb/issues/2007

arkivanov commented 1 year ago

Web (Canvas) support is now enabled. The new branch for all experimental targets is compose-experimental.

blueberry404 commented 1 year ago

Hi @arkivanov, first of all thank you to create this amazing library. I have been trying to integrate Decompose in Kotlin Multiplatform Compose project. I tried my best to thoroughly review your documentation and samples. I am unable to resolve following error on iOS:

Task :shared:linkPodDebugFrameworkIosX64 FAILED error: Following dependencies exported in the podDebugFramework binary are not specified as API-dependencies of a corresponding source set: FAILURE: Build failed with an exception.

  • What went wrong: Execution failed for task ':shared:linkPodDebugFrameworkIosX64'.

    Following dependencies exported in the podDebugFramework binary are not specified as API-dependencies of a corresponding source set:

    Files: [/Users/Admin/.gradle/caches/modules-2/files-2.1/com.arkivanov.decompose/decompose-iosx64/2.0.0-alpha-02/c681ede152c090cec61131c93a90d75bac1c43fc/decompose.klib]

    Please add them in the API-dependencies and rerun the build.

Here are the chunks of my build.gradle.kts:

cocoapods {
        ....
        framework {
            baseName = "shared"
            isStatic = true

            export("com.arkivanov.decompose:decompose:2.0.0-alpha-02")
            export("com.arkivanov.essenty:lifecycle:1.1.0")
        }
        extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
    }

sourceSets {
        val commonMain by getting {
           implementation("com.arkivanov.decompose:decompose:2.0.0-alpha-02")
           implementation("com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-compose-experimental-alpha-02")
         }
}

I also tried to add dependencies like here, but no luck probably because it doesn't use cocoapods.

Please let me know if I am missing anything. Thank you.

arkivanov commented 1 year ago

@blueberry404 any module that is exported to the framework must be specific as api dependency, not implementation. You also missing the dependencies {} block. See the example.

blueberry404 commented 1 year ago

Thank you for quick response. Sorry, missing dependency block was pasting typo at my end. Key was to use api dependencies for both in iOSMain. I am able to compile it now. Thank you!

dependencies {
                api("com.arkivanov.decompose:decompose:$decompose")
                api("com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-compose-experimental-alpha-02")
            }
arkivanov commented 1 year ago

Awesome! Glad it worked. Most likely you don't need to export extensions-compose-jetbrains, as it contains extensions specifically for Compose.

blueberry404 commented 1 year ago

Actually, I am going to write the common compose code for Android and iOS using jetbrain's compose, therefore I thought this is also I needed.

arkivanov commented 1 year ago

I think exporting is only required if you want co use that APIs in the iOS project (Xcode). If you are just using it from Kotlin, then you shouldn't need to export Compose extensions. Just implementation should enough. You can try at least.

blueberry404 commented 1 year ago

Ok I get your point now. I will try and let you know. Thank you!

blueberry404 commented 1 year ago

Hi @arkivanov, I had a busy weekend so wasn't able to check it out. First of all, I would like to disregard my previous compilation comment because it actually compiled on android studio only, but not on xcode-iOS if I add following to the iOSMain. Sorry, I was in hurry so wasn't able to properly test on iOS at that time.

api("com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-compose-experimental-alpha-02")

As you asked, I removed above dependency and it worked perfectly for the first time. Then I added more code to test the navigation. It's working completely fine on Android, but compilation has been broken on iOS end. LifecycleRegistry related classes are detected, but not Decompose ones. Also, somehow class DecomposeDefaultComponentContext is not generated anymore.

import Foundation
import shared

@objc
class LifecycleManager: NSObject, ObservableObject {
    let lifecycle: LifecycleRegistry
    let root: MyRootComponent

    override init() {
        lifecycle = LifecycleRegistryKt.LifecycleRegistry()

        root = DefaultMyRootComponent(
            componentContext: DecomposeDefaultComponentContext(lifecycle: lifecycle))
        LifecycleRegistryExtKt.create(lifecycle)
    }

    deinit {
        LifecycleRegistryExtKt.destroy(lifecycle)
    }
}

In the shared.h, I was only able to find its protocol, but not the implementation.

Screenshot 2023-05-09 at 1 18 37 AM

I commented out all the code related to shared and Decompose, stashed my newly added code to revert to last working state in the hope that correct files would be generated. Even cleared Derived Data, build folder multiple times of both KMM and iOS but it's not generating required files in framework. I am wondering how it worked for first time now 😄

Again, here are chunks of build.gradle in shared module:

cocoapods {
       ...
        framework {
            baseName = "shared"
            isStatic = true

            export("com.arkivanov.decompose:decompose:$decompose")
            export("com.arkivanov.essenty:lifecycle:1.1.0")
        }
        extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
    }

    val iosMain by creating {
            dependsOn(commonMain)
            dependencies {
                api("com.arkivanov.decompose:decompose:$decompose")
            }
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
        }
arkivanov commented 1 year ago

It's hard to determine what's the cause. You certainly need api("com.arkivanov.essenty:lifecycle:1.1.0") in your iosMain dependencies. Also you can try cleaning your project and rebuilding it with --rerun-tasks flag. Otherwise, it could be something related to cocoapods integration. It doesn't look specific to Decompose actually.

wman1980 commented 1 year ago

I ran into a similar issue. Maybe this thread at slack helps: https://kotlinlang.slack.com/archives/C03H3N51SKT/p1683725214780229

dQqAn commented 1 year ago

Hello, i have a error for implementation("com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02") in the compose multiplatform. Can u help me? @arkivanov

Error:

A problem occurred configuring project ':common'.

Could not resolve all dependencies for configuration ':common:iosArm64CompileKlibraries'. Could not resolve com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02. Required by: project :common No matching variant of com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 was found. The consumer was configured to find a usage of 'kotlin-api' of a library, preferably optimized for non-jvm, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native', attribute 'org.jetbrains.kotlin.native.target' with value 'ios_arm64' but:

  • Variant 'debugApiElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares an API of a library:
  • Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'debugRuntimeElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a runtime of a library:
  • Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'debugSourcesElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a runtime of a component:
  • Incompatible because this component declares documentation, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a library, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'jvmApiElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares an API of a library:
  • Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'jvmRuntimeElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a runtime of a library:
  • Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'jvmSourcesElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a runtime of a component:
  • Incompatible because this component declares documentation, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' and the consumer needed a library, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'metadataApiElements' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a library:
  • Incompatible because this component declares a usage of 'kotlin-metadata' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'common' and the consumer needed a usage of 'kotlin-api' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'metadataSourcesElements' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02:
  • Incompatible because this component declares a usage of 'kotlin-runtime' of documentation, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'common' and the consumer needed a usage of 'kotlin-api' of a library, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'releaseApiElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares an API of a library:
  • Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'releaseRuntimeElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a runtime of a library:
  • Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')
  • Variant 'releaseSourcesElements-published' capability com.arkivanov.decompose:extensions-compose-jetbrains:2.0.0-alpha-02 declares a runtime of a component:
  • Incompatible because this component declares documentation, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a library, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native'
  • Other compatible attributes:
  • Doesn't say anything about its target Java environment (preferred optimized for non-jvm)
  • Doesn't say anything about org.jetbrains.kotlin.native.target (required 'ios_arm64')

--In addition, when i update the versions of implementation("com.arkivanov.decompose:decompose:2.1.0-compose-experimental-alpha-01") and implementation("com.arkivanov.decompose:extensions-compose-jetbrains:2.1.0-compose-experimental-alpha-01"), I have a different error and the above error is gone. The new error: (in terminal with ./gradlew build --warning-mode all --stacktrace)

Execution failed for task ':android:checkDebugAarMetadata'.

A failure occurred while executing com.android.build.gradle.internal.tasks.CheckAarMetadataWorkAction 4 issues were found when checking AAR metadata:

  1. Dependency 'com.arkivanov.essenty:back-handler-android-debug:1.2.0-alpha-01' requires libraries and applications that depend on it to compile against codename "UpsideDownCake" of the Android APIs.

    :android is currently compiled against android-33.

    Recommended action: Use a different version of dependency 'com.arkivanov.essenty:back-handler-android-debug:1.2.0-alpha-01', or set compileSdkPreview to "UpsideDownCake" in your build.gradle file if you intend to experiment with that preview SDK.

  2. Dependency 'androidx.activity:activity:1.8.0-alpha04' requires libraries and applications that depend on it to compile against codename "UpsideDownCake" of the Android APIs.

    :android is currently compiled against android-33.

    Recommended action: Use a different version of dependency 'androidx.activity:activity:1.8.0-alpha04', or set compileSdkPreview to "UpsideDownCake" in your build.gradle file if you intend to experiment with that preview SDK.

  3. Dependency 'androidx.activity:activity-ktx:1.8.0-alpha04' requires libraries and applications that depend on it to compile against codename "UpsideDownCake" of the Android APIs.

    :android is currently compiled against android-33.

    Recommended action: Use a different version of dependency 'androidx.activity:activity-ktx:1.8.0-alpha04', or set compileSdkPreview to "UpsideDownCake" in your build.gradle file if you intend to experiment with that preview SDK.

  4. Dependency 'androidx.activity:activity-compose:1.8.0-alpha04' requires libraries and applications that depend on it to compile against codename "UpsideDownCake" of the Android APIs.

    :android is currently compiled against android-33.

arkivanov commented 1 year ago

Regarding the first error, I think you should use experiment-compose version if you support iOS. Try using 2.0.0-experimental-compose-beta-01.

Regarding the second error, as mentioned in the release notes, 2.1.0-alpha versions are compiled with compileSdkPreview = UpsideDownCake. This is currently required for the new predictive back gesture on Android U. So, either enable that in your project as well, or use 2.0.0-beta-01.

dQqAn commented 1 year ago

Thanks for your answer. Decompose version with "2.0.0-compose-experimental-beta-01" is perfect. Everything is perfect.

arkivanov commented 1 year ago

Let's also track wasm here, from #375.

chrisjenx commented 1 year ago

Just checking if my lifecycle hook into iOS compose is correct, I adapted the SwiftUI to the iOS compose example:

struct iOSApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self)
    var appDelegate: AppDelegate

    var rootHolder: AppHolder { appDelegate.appHolder }

    var body: some Scene {
        WindowGroup {
            ContentView(root: rootHolder.root)
        }
    }
}
import Foundation
import shared

class AppHolder : ObservableObject {

    let lifecycle: LifecycleRegistry
    let root: AppComponent

    init() {
        lifecycle = LifecycleRegistryKt.LifecycleRegistry()
        root = DefaultAppComponent(
            componentContext: DefaultComponentContext(lifecycle: lifecycle)
        )
        LifecycleRegistryExtKt.create(lifecycle)
    }

    deinit {
        // Destroy the root component before it is deallocated
        LifecycleRegistryExtKt.destroy(lifecycle)
    }
}
struct ComposeView: UIViewControllerRepresentable {

    var root: AppComponent

    func makeUIViewController(context: Context) -> UIViewController {
        Main_iosKt.MainViewController(root: root)
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    var root: AppComponent

    var body: some View {
        ComposeView(root: root)
            .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
    }
}
import Foundation
import UIKit

class AppDelegate: NSObject, UIApplicationDelegate {
    let appHolder: AppHolder = AppHolder()
}

It seems to compile and work, I obviously missed out this part:

.onChange(of: scenePhase) { newPhase in
                    switch newPhase {
                    case .background: LifecycleRegistryExtKt.stop(rootHolder.lifecycle)
                    case .inactive: LifecycleRegistryExtKt.pause(rootHolder.lifecycle)
                    case .active: LifecycleRegistryExtKt.resume(rootHolder.lifecycle)
                    @unknown default: break
                    }
                }

Not sure if thats needed for iOS Compose?

arkivanov commented 1 year ago

Looks correct @chrisjenx. Just one possible point to improve in case you find it useful. I find it cleaner to destroy the root component on willTerminateNotification, instead of the deinit block. Check out the example here.

rezyfr commented 12 months ago

linkPodDebugFrameworkIosX64 FAILED I also had above reported error for several hours with the correct setup like on the example

turns out I use the 1.0.0 version for the essenty dependency, and it finally can compile after I update it to 1.1.0, just in case anyone has the same issue as me :)

chrisjenx commented 9 months ago

I changed to using the new AutomaticLifecycle, also not tried web yet, but we're using iOS in production now, not seen anything major that jumps out, need to test out persistance, but imo it seems pretty stable, iOS can probably not be considered "experimental" esp after moving to serilization plugin

arkivanov commented 9 months ago

Oh, it was only considered experimental because it was defined so by JetBrains. Now since we don't need org.jetbrains.compose.experimental.uikit.enabled=true anymore, the support of Compose for iOS will be moved to the stable branch in v3.0. Being tracked here: https://github.com/arkivanov/Decompose/issues/451.

chrisjenx commented 9 months ago

Awesome, thank you for your superb work!

markvtailor commented 9 months ago

Hi! I'm trying to implement shared navigation in my KMP project with shared UI. It works fine with Android, but I encountered an error with iOS:

error: Overload resolution ambiguity: public fun <C : Any, T : Any> Children(stack: ChildStack<TypeVariable(C), TypeVariable(T)>, modifier: Modifier = ..., animation: StackAnimation<TypeVariable(C), TypeVariable(T)>? = ..., content: (child: Child.Created<TypeVariable(C), TypeVariable(T)>) -> Unit): Unit defined in com.arkivanov.decompose.extensions.compose.jetbrains.stack public fun <C : Any, T : Any> Children(stack: Value<ChildStack<TypeVariable(C), TypeVariable(T)>>, modifier: Modifier = ..., animation: StackAnimation<TypeVariable(C), TypeVariable(T)>? = ..., content: (child: Child.Created<TypeVariable(C), TypeVariable(T)>) -> Unit): Unit defined in com.arkivanov.decompose.extensions.compose.jetbrains.stack

App.kt:34:9: error: Cannot infer a type for this parameter. Please specify it explicitly.

App.kt:36:50: error: @Composable invocations can only happen from the context of a @Composable function

App.kt:36:50: error: @Composable invocations can only happen from the context of a @Composable function

RootComponent.kt:24:32: error: Type mismatch: inferred type is Any but com.arkivanov.essenty.parcelable.Parcelable / = com.arkivanov.parcelize.darwin.Parcelable / was expected

RootComponent.kt:24:32: error: Type mismatch: inferred type is Any but RootComponent.Configuration was expected

I suspect there is some problems with iOS dependencies, but I was unable to track the exact cause. My build.gradle looks like this. Please let me know if I am missing anything. Thank you.

kotlin {
    targetHierarchy.default()
    android {
        compilations.all {
            kotlinOptions {
                jvmTarget = "1.8"
            }
        }
    }
listOf(
    iosX64(),
    iosArm64(),
    iosSimulatorArm64()
).forEach {
    it.binaries.framework {
        baseName = "shared"
        isStatic = true
        val decompose_version = "2.2.0-compose-experimental"
        val essenty_version = "1.3.0"

        export("com.arkivanov.decompose:decompose:$decompose_version")
        export("com.arkivanov.essenty:lifecycle:$essenty_version")

        // Optional, only if you need state preservation on Darwin (Apple) targets
        export("com.arkivanov.essenty:state-keeper:$essenty_version")
    }
}

sourceSets {
    val androidMain by getting {
        dependencies {
            implementation ("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
        }
        dependsOn(commonMain.get())
    }

    named("commonMain") {
        dependencies {
            api(compose.foundation)
            api(compose.animation)
            val decomposeVersion = "2.2.0"
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
            implementation("com.arkivanov.decompose:decompose:$decomposeVersion-compose-experimental")
            implementation("com.arkivanov.decompose:extensions-compose-jetbrains:$decomposeVersion-compose-experimental")
            implementation("com.russhwolf:multiplatform-settings:1.1.1")
            implementation("com.russhwolf:multiplatform-settings-no-arg:1.1.1")
            implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")
            implementation("com.moriatsushi.insetsx:insetsx:0.1.0-alpha10")
            //put your multiplatform dependencies here
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material)
            @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
            implementation(compose.components.resources)
            implementation("dev.icerock.moko:resources:0.23.0")
            implementation("dev.icerock.moko:resources-compose:0.23.0")
            implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
        }
    }
}

sourceSets {
    named("iosMain") {
        dependencies {
            val decompose_version = "2.2.0-compose-experimental"
            val essenty_version = "1.3.0"
            api("com.arkivanov.decompose:decompose:$decompose_version")
            api("com.arkivanov.essenty:lifecycle:$essenty_version")
        }
    }
}
arkivanov commented 9 months ago

@markvtailor it's difficult to help without the reproducer or at least the code. Feel free to file a separate issue with the additional information.

arkivanov commented 9 months ago

Starting with Decompose 3.0.0-alpha01 all Compose variants are supported without any version suffixes.

chrisjenx commented 9 months ago

🥳 Nice work!

malliaridis commented 5 months ago

@arkivanov are the Docs outdated about WASM (see here)? May be addressed in #610 if that is the case.

arkivanov commented 5 months ago

Thanks for pointing that out! The docs should be fixed now.