JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.21k stars 1.17k forks source link

Requirement of `isStatic = true` #3178

Closed haydenkaizeta closed 1 year ago

haydenkaizeta commented 1 year ago

When setting

cocoapods {
    ..
    framework {
        ..
        isStatic = false //(the default)
    }
}

and try to build an iOS app I get a long list of messages ending in:

  SkCTFontDescriptorGetSkFontStyle(__CTFontDescriptor const*, bool) in libskia.a(fontmgr_mac_ct.SkTypeface_mac_ct.o)
ld: symbol(s) not found for architecture arm64

To my knowledge isStatic=false is required to get SQLDelight working on multiplatform, so it would be great to resolve this.

Affected platforms Select one of the platforms below:

Versions

To Reproduce Steps and/or the code snippet to reproduce the behavior:

  1. Remove isStatic = true from iOS sample project
  2. Build the iOS app
  3. Build fails

Expected behavior

  1. Should be able to remove it and build
akiya-nagatsuka commented 1 year ago

Getting the same issue on iOS when running unit tests on iOS even with isStatic = true.

dam5s commented 1 year ago

I have the same problem with isStatic = true on my M2 Mac during :shared:linkDebugTestIosArm64. A colleague with the same codebase on M1 Mac is able to build it just fine.

akiya-nagatsuka commented 1 year ago

Interesting, I am also using M2

dam5s commented 1 year ago

Correction, it doesn't build on any of my colleagues machines. That includes M1, M2 and Intel macs.

warnyul commented 1 year ago

You got this issue because you use types on your public API from skia lib, and skia is not exported. However, you do not want to export skia, because that will produce a large framework.

Better just remove all of the compose-related class references from your public interfaces. Because you do not want to export skia or compose.

haydenkaizeta commented 1 year ago

@warnyul what you you mean by public API? If I take the example multiplatform project and set that to false it gives me that build error. I need it to be false to get SQLDelight working. At this point in time I would also be willing to export skia but that seemed like a massive hassle, and it seems compose should not require setting the isStatic variable to true long term since KMP decided to set it to false

pablichjenkov commented 1 year ago

You can try having cocoapods and SQDelight in different modules. Integrate SQDelight in shared and have another module named iosEntryPoint or iosAppKt that depends on shared. The iosAppKt is the one that uses cocoapods to build your framework to be used in iosApp swift module.

sw-dev1 commented 1 year ago

@pablichjenkov Seems like your suggestion is the only way of having SQLDelight and Compose Multiplatform in one project. Could you elaborate on how the Compose Multiplatform template should be modified to accommodate this? How does the build.gradle look like for the module named iosEntryPoint? Is this module also "shared"?

chrisbanes commented 1 year ago

FYI: I'm successfully using SQLDelight v2.0.0-rc02 and Compose MP in https://github.com/chrisbanes/tivi with isStatic = true

pablichjenkov commented 1 year ago

Hi @sw-dev1 . You can structure the module dependency in many ways. Just make sure that SQDelight and cocoapods plugins are not in the same module. See bellow for 2 examples

1- Create a new module(copy-paste) and just have commonMain and iOSMain as targets. Name this module iOSKotlinBridge or whatever name you want. Make the swift-Xcode project depend on the framework produced by the cocoapods plugin in iOSKotlinBridge. Then make iOSKotlinBridge depend on module shared where you have the SQdelight integration. Module dependency will look like: Xcode -> iOSKotlinBridge(podframework by cocoa plugin) -> shared(sqdelight)

2- Create a new module and name it whatever name you want to, lets say my-service-x-api. These module will have all the targets you wish to handle. Integrate sqdelight and ktor here. Then use cocoapods in module shared and make module shared depend on my-service-x-api. Then your Xcode project will depend on shared as conventional. Check link bellow for this pattern https://github.com/pablichjenkov/kmp-amadeus-api Module dependency will look like: Xcode -> shared(podframework by cocoa plugin) -> my-service-x-api(sqdelight)

I prefer number 2 because the convention of using shared as the common ui module where the different platform modules will depend on. But you can pick whatever structure you like. It is all about module inter-dependency. I advice checking above repo by chrisbanes. It is very usefull.

pablichjenkov commented 1 year ago

By the way @sw-dev1 , this project seems to mix both plugins(cocoa + SQDelight ) in the same module with no issue. Check what you have different. https://github.com/daniaviladomingo/kmm/blob/master/composeApp/build.gradle.kts

Mugurell commented 1 year ago

By the way @sw-dev1 , this project seems to mix both plugins(cocoa + SQDelight ) in the same module with no issue. Check what you have different. https://github.com/daniaviladomingo/kmm/blob/master/composeApp/build.gradle.kts

Think the difference is that this uses isStatic = true.

sw-dev1 commented 1 year ago

Maybe the problem as the title says here (Requirement of isStatic = true) is due to sqldelight coroutines extensions. The example daniaviladomingo avoids using them by wrapping the database queries in additional coroutines, thus can compile CMP and SQLDelight MP on iOS with one static cocoapod. The example by chrisbanes is using sqldelight coroutines extensions, but employs additionally Room as a database as well as many modules, some similarly named like "common" and "shared". Is this level of complexity necessary in order to achieve simple common front-end and back-end (UI and database) on Android and iOS?

chrisbanes commented 1 year ago

The example chrisbanes is using sqldelight coroutines extensions, but employs additionally Room as a database as well as many modules, some looking similarly like "common" and "shared".

Nope, Tivi uses SQLDelight on all platforms (Android, iOS, JVM). Room isn't used at all (as of about 4 months ago).

haydenkaizeta commented 1 year ago

@pablichjenkov indeed, taking your project as a starter example I was able to get SQLDelight+Compose working on iOS and Android. Many thanks!

pablichjenkov commented 1 year ago

Thanks @haydenkaizeta glad to hear that it helped you. I use it as a template whenever consuming a new service API. Feel free to file an issue if you find a problem or contribute to it if you want new functionality. 👍🏻

pjBooms commented 1 year ago

Fixed in 1.5.0-beta01

sw-dev1 commented 1 year ago

Hi @pablichjenkov, thanks for your detailed elaboration. I followed this

2- Create a new module and name it whatever name you want to, lets say my-service-x-api. These module will have all the targets you wish to handle. Integrate sqdelight and ktor here. Then use cocoapods in module shared and make module shared depend on my-service-x-api. Then your Xcode project will depend on shared as conventional. Check link bellow for this pattern https://github.com/pablichjenkov/kmp-amadeus-api Module dependency will look like: Xcode -> shared(podframework by cocoa plugin) -> my-service-x-api(sqdelight)

adapting it to Xcode -> shared(podframework by cocoa plugin) -> sharedDatai(sqdelight).

Problem: Xcode does not recognize the classes in sharedData.

Let me note that they are not recognized in the android app either via the interdependency shared -> sharedData. I had to add the dependency on sharedData as a direct dependency to the build.gradle.ktx of the android app. Looking at your example https://github.com/pablichjenkov/kmp-amadeus-api you are doing that, too. Where does the understanding come from that for Android the interdependency on sharedData via commonMain in shared is ignored but for iOS it is not?

pablichjenkov commented 1 year ago

Hi @sw-dev1 , in general that is the way transitive dependencies work, unless you use something different, in gradle for instance: api() , which will expose your library dependencies in the compile classpath of your library's client. I really prefer not exposing my dependencies from the library but make the client use them explicitly. But in any case, you should be able to do the same using cocoapods or swift package manager.

sw-dev1 commented 1 year ago

I can confirm with Compose Multiplatform (CMP) 1.5.0-beta01 isStatic=true is no longer required for CMP. Thus one cocoapod can now host both CMP and SQLDelight. Note: iOS build works only from Xcode, not from AS Giraffe | 2022.3.1

sw-dev1 commented 1 year ago

Hi @pablichjenkov, thanks again for your efforts. Now that this issue is closed providing a straightforward solution (with )1.5.0-beta01 isStatic=true is no longer required for CMP), a workaround Is no longer necessary.

But I would like to understand why your solution did not work for me.

Your iOS app depends on AmadeusDemoKt(the name of the amadeus-api cocoapod) only, and not on shared, so it is 1), but shared depends on amadeus-api, so it is 2). Do you see it differently?

Neither CMP depends on SQLDelight nor vice versa. Looks to me that introducing an artificial dependency layer works around the problem that KMM cannot build and link two separate frameworks for iOS: one cocoapod and one SPM or alternatively two cocoapods or SPM. Am I mistaken?

pablichjenkov commented 1 year ago

Hi @sw-dev1 , Point 1:

Your iOS app depends on AmadeusDemoKt(the name of the amadeus-api cocoapod) only, and not on shared

AmadeusDemoKt is the ios framework created by the shared module so I do directly depend on it. I just changed the default name, check on the build.gradle file the following code. But the iOS App indeed depends on the shared module.

framework {
     baseName = "AmadeusDemoKt". // <-- custom name , default name is 'shared'
     isStatic = true
}

Point 2: That is correct, shared depends on amadeus-api

In regards to the this:

Neither CMP depends on SQLDelight nor vice versa. Looks to me that introducing an artificial dependency layer works around the problem that KMM cannot build and link two separate frameworks for iOS: one cocoapod and one SPM or alternatively two cocoapods or SPM. Am I mistaken?

Well, actually I don't have my module interrelation layout that way because of the limitation described in this ticket. I actually have this structure because of a CLEAN architecture thing. I want my data layer separated from the UI layer. The shared module I just use it for UI, the different services and data related stuff I keep it in other modules/libraries. I advice you doing the same the benefits are countless.

sw-dev1 commented 1 year ago

Hi @pablichjenkov, thanks again for you quick response.
OK, the module shared is accessible in Android as module shared and it is accessible in iOS as framework AmadeusDemoKt. The module amadeus-api is accessible in Android as module amadeus-api and it is accessible in iOS how?

pablichjenkov commented 1 year ago

Hi @sw-dev1 , In Android, JVM and JS, the library which shared module depends on, is not included as part of the .JAR/.JS build output but in the case of iOS, if shared depends on implementation(project("local-project")) the dependency is included in the output xcframework. If you take a look under
shared/build/bin/iosArm64/podDebugFramework/AmadeusDemoKt.framework/Headers/AmadeusDemoKt.h you see the different classes from the amadeus-api module are present there, with a prefix(Amadeus_api). The task that produces the xcframework seems to figure out that is a local project and package it along with shared stuff. That is what I see in practicality, don't ask me for the theory behind 🤷‍♂️. That is why the iOSApp can see some classes of the amadeus-api module. Although to be honest this is a design flaw that I have to fix. The application modules should only see shared and not knowing anything about amadeus-api I just gotta fix it in Android, JVM and JS by doing the same I do in the iOS case, writing the code that creates the database in the shared module, platform specific sourceset.

sw-dev1 commented 1 year ago

Hi @pablichjenkov, This is clear now. Thanks for your openness.

I would like to work with your example https://github.com/pablichjenkov/kmp-amadeus-api, which I also find very useful, but it doesn't compile on my machine (MBP M1) and Xcode cannot open the iOS app. I hope it is OK if I open a ticket in that repo? Secondly, you have a very good point about clean code. In my humble opinion KMP poses some challenges on that. Which repo or forum would be appropriate to open (or join) a discussion about it?

pablichjenkov commented 1 year ago

Hi @sw-dev1 Great to hear! The idea is having a "copy/paste" or "git clone" kinda project that you would use anytime you need to consume an API. Yeah, open the issues there, to not overwhelm this thread.

In regards to the CLEAN arch I believe KMP helps because it forces you to rely on abstraction and that is a big win. It doesn't let you take platform shortcuts no. The nature of being multiplatform enforces abstraction, and I see that really good.

okushnikov commented 3 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.