Loading repository dataβ¦
Loading repository dataβ¦
gchristov / repository
π This project leverages Kotlin Multiplatform Mobile (KMM) for shared code, building a single codebase for a native iOS app utilizing SwiftUI and a native Android app using Jetpack Compose. This approach promotes code reusability and simplifies app maintenance across both platforms.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
Newsfeed app with endless scrolling built for Kotlin Multiplatform (iOS + Android).
The project setup is quite straightforward:
Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle and set Gradle JDK to be Embedded JDK.appAndroid configuration.appIos.xcworkspace with XcodeappIos scheme.The project could also be used as a template for other multiplatform apps, providing a solid foundation to build on:
chmod +x new_app_setup.sh./new_app_setup.sh and follow the instructionsNote that you might need to replace the API key for this project if this link doesn't work. To get a new key, go to this link and register a developer account. Once you get the key, replace the value for API_KEY in kmm/kmm-common-network/build.gradle.kts.
The project is built using CLEAN multi-module architecture consisting of feature, data and test-fixture modules both for the shared KMM code and the individual client targets. Each module contains classes and resources only related to its functionality and some modules can be reused within other modules. Definitions of the module types can be found under each platform below.
To allow easier differentiation between modules, there are separate root folders for each of the supported platforms:
kmm - shared code and related modulesappAndroid - Android app and related modulesappIos - iOS app and related modulesKMM is using Gradle as a build system so the project's build process is setup differently depending on the client target that is being compiled:
Ths shared multiplatform code consists of 4 different module types:
common - modules with common setup that are meant to be extended or used within other modules. Some examples include networking, dependency injection, persistence.data - modules that are responsible for data retrieval and persistence for a specific app feature. We usually have one data module per app feature.feature - modules that contain the view-models and core business logic for a specific app feature. We usually have one feature module per app feature.test-fixtures - modules that contain test fake's for a particular feature module. We usually have one test fixtures module per app feature.To make creating new modules seamless, the KMM setup provides 3 dedicated Kotlin DSL Gradle plugins that should be applied to new modules depending on their type:
common modules should apply the id("kmm-module-plugin") plugin, unless they are meant to be exposed through kmm-module-plugin itself, in which case they should apply the id("kmm-platform-plugin") plugin to avoid circular dependencies;data modules should apply the id("kmm-data-plugin") plugin;feature modules should apply the id("kmm-feature-plugin") plugin;id("kmm-module-plugin") plugin;You can create new modules using Android Studio's File -> New Module. Keep 3 things in mind when adding new KMM modules:
kmm folder.kmm-.modules.gradle.kts and add the new module to the list of modules.The Android app is also using Gradle as a build system and consists of 3 different module types:
common - modules with common setup that are meant to be extended or used within other modules. Some examples include Jetpack Compose, the design system, tests.feature - modules that contain the UI for a specific app feature. We usually have one feature module per app feature.test-fixtures - modules that contain test robots for a particular feature module. We usually have one test fixtures module and robot per app feature.Similarly to KMM, to make creating new modules seamless, the Android setup provides 2 dedicated Kotlin DSL Gradle plugins that should be applied to new modules depending on their type:
common modules should apply the id("android-module-plugin") plugin, unless they are meant to be exposed through android-module-plugin itself, in which case they should apply the id("android-library-plugin") plugin to avoid circular dependencies;feature modules should apply the id("android-feature-plugin") plugin;id("android-module-plugin") plugin;You can create new modules using Android Studio's File -> New Module. Keep 3 things in mind when adding new Android modules:
appAndroid folder.modules.gradle.kts and add the new module to the list of modules.implementation(projects.MY_KMM_MODULE) dependency.The iOS app has its own native build system using Xcode and is using the concept of a "workspace" to define a CLEAN multi-module setup consisting of 3 different module types with the exact same definitions as the ones for Android above.
The only real difference is around linking the KMM dependencies which are specified through an iOS .framework that has to be linked (or embedded) to Xcode so that it can be accessed correctly from Swift in the final app package. Since this process is a bit more involved, we have provided an overview of the 3 key concepts required to achieve this:
embedAndSignAppleFrameworkForXcodeRun ScriptsFramework Search PathsThis Gradle task is specifically designed to run as part of the Xcode build process and its core purpose is to compile the Kotlin source files into Swift, generate the .framework file, link and sign it with Xcode, as the name suggests. It should be invoked from a Run Script phase during every Xcode build to generate a .framework file for one Kotlin dependency module. For example, ./gradlew :kmm-umbrella:embedAndSignAppleFrameworkForXcode will compile all code in the shared kmm-umbrella module only and generate its framework based on the specs in kmm/kmm-umbrella/build.gradle.kts. This framework follows Gradle's rules for exporting dependencies and all api dependencies will be visible to Swift. This includes all api submodules that kmm-umbrella depends on.
A caveat with submodule api dependencies is that if a module (A) declares a dependency on module (B), the generated Swift code will prefix the classes from B with its fully qualified module name when they are exposed through A. For example, a class MyClass defined in B but exposed through A will be available as BMyClass when the A.framework is used in Swift. In addition, because the individual modules are exported as separate .frameworks, they do not share memory and resources, unlike Android, where they are all part of the same app memory model.
To work around this, the general KMM advice is to export an umbrella .framework for Xcode containing all modules that should be exposed to iOS and Swift. This method overcomes the two issues above:
export() function, the module's transitive api dependencies are correctly exported to Swift with their module-independent names, e.g. in our example above, MyClass which is exposed through A but defined in B will be available as MyClass to swift when the A.framework is linked;Run Scripts are custom scripts that can be invoked during an Xcode build during certain stages. In terms of KMM, we require a custom Run Script with which to invoke the embedAndSignAppleFrameworkForXcode Gradle task to generate and link the .framework file. A caveat here is that embedAndSignAppleFrameworkForXcode has to ideally be invoked from the main appIos target to ensure that the code is signed with the correct signature, otherwise Xcode will throw an error. For simple apps, this should be okay.
In this project, we have a multi-module setup so we have actually linked a Run Script phase both for the appIos target (to sign the code correctly) and for the relevant feature modules (where the KmmShared framework is used). This is because Xcode compiles the code using Dependency Order by default which means that the main app files will be compiled last. Therefore, if we do not have the Run Scripts in each feature module, we will get Swift compile errors related to a missing KmmShared module which is indeed the case because the app's Run Script will run after all sub-modules have been compiled first. Of course, having two Run Script phases means the KMM code will be compiled twice (or more times, depending on how many feature modules we have) when appIos is built, but luckily Gradle's cache makes subsequent compilations run almost instantly so there isn't much overhead.
Since the generated KmmShared.framework isn't directly linked to Xcode, the project needs a way to locate it when referenced from Swift. This is where Framework Search Paths have to be used to tell Xcode where the KmmShared framework is. An example value for Framework Search Paths is:
$(SRCROOT)/../../kmm/kmm-umbrella/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)
For this example, we will assume that we have a new shared KMM module in Kotlin under kmm/kmm-settings which is ready to be integrated with Xcode to build a settings feature for the newsfeed app (containing the view-model and all shared code to drive the UI).
exportedDependencies in the umbrella framework under kmm/kmm-umbrella/build.gradle.kts, as projects.kmmSettings.appIos.xcworkspace.Framework project called settings (without tests for now, more on that in the com