A Kotlin Multiplatform (KMP) Application featuring MVI Architecture, Modular Design, and Automated CI/CD.
| Mobile | Web | Desktop |
![]() |
![]() |
![]() |
OmniHub serves as the client-side implementation of a modern multiplatform ecosystem. It is built strictly as a UI & Presentation Layer application, consuming core business logic from its companion library, OmniFeed.
This project demonstrates how to decouple UI from logic in a Cross-Platform environment, ensuring that the Android, iOS, Desktop, and Web clients remain thin, reactive, and consistent.
The project adopts a Modular Clean Architecture with MVI (Model-View-Intent) pattern:
- Presentation Layer (OmniHub):
- Built with Compose Multiplatform.
- Implements MVI pattern: UI observes a single
UiStateand dispatchesIntents(Events). - Platform Specifics: Handles platform-unique entry points (Activities, ViewControllers) and Deep Link redirection.
- Domain & Data Layers (Encapsulated in [OmniFeed Library]):
- All business rules, Use Cases, and Repository implementations are strictly isolated in the external
OmniFeedlibrary. - This separation allows the core logic to be versioned, tested, and reused independently of the UI.
- All business rules, Use Cases, and Repository implementations are strictly isolated in the external
- State Management: Kotlin Flows & Coroutines (StateFlow/SharedFlow).
- Dependency Injection: Koin for centralized dependency management across all platforms.
- Navigation: Type-safe navigation handling across platforms.
- CI/CD: GitHub Actions with Semantic Release for automated versioning.
Instead of relying on heavy, platform-specific SDKs, OmniHub implements a lightweight Deep Link Coordination system for authentication:
- Universal Redirect Handling: The app utilizes a shared
DeepLinkBufferto capture OAuth callbacks from the system browser. - Platform Agnostic: Whether on Android (Intent Filter), iOS (Universal Links), or Desktop, the authentication flow remains consistent and testable.
A key architectural decision was to forbid any direct data manipulation in the UI layer.
- OmniHub (this repo) contains zero business logic. It only renders State and passes User Intents.
- OmniFeed (core lib) handles all API interactions (Unsplash API), token management, and data persistence.
The project adheres to Conventional Commits to drive a fully automated release cycle:
- Semantic Release: Automatically analyzes commits to determine the next version number (Patch/Minor/Major).
- Changelog Generation: Automatically generates release notes based on the commit history.
composecommonMain: Shared UI components, Screens, and ViewModels (MVI).androidMain/iosMain: Platform entry points andDeepLinkinterception logic.
.github/workflows: CI/CD definitions for automated testing and releasing.gradle/libs.versions.toml: Centralized dependency management.
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :compose:assembleDebug
- on Windows
.\gradlew.bat :compose:assembleDebug
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :compose:run
- on Windows
.\gradlew.bat :compose:run
To build and run the development version of the web app, use the run configuration from the run widget in your IDE's toolbar or run it directly from the terminal:
- for the Wasm target (faster, modern browsers):
- on macOS/Linux
./gradlew :compose:wasmJsBrowserDevelopmentRun
- on Windows
.\gradlew.bat :compose:wasmJsBrowserDevelopmentRun
- on macOS/Linux
- for the JS target (slower, supports older browsers):
- on macOS/Linux
./gradlew :compose:jsBrowserDevelopmentRun
- on Windows
.\gradlew.bat :compose:jsBrowserDevelopmentRun
- on macOS/Linux
This project uses the Kotlin Multiplatform CocoaPods plugin to handle iOS dependencies.
Prerequisites:
-
Install CocoaPods if you haven't already:
sudo gem install cocoapods
or use
Homebrewif you have it installed:brew install cocoapods
-
In the
gradle/libs.versions.tomlfile, add the Kotlin CocoaPods Gradle plugin to the[plugins]block:kotlinNativeCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
-
Navigate to the root
build.gradle.ktsfile of your project and add the following alias to theplugins {}block:alias(libs.plugins.kotlinNativeCocoapods) apply false -
Open the module where you want to integrate CocoaPods (e.g., the
composemodule), and add the following alias to theplugins {}block of thebuild.gradle.ktsfile:alias(libs.plugins.kotlinNativeCocoapods)
Setup: Follow these steps for initial setup or after modifying dependencies:
-
Navigate to the
iosAppdirectory:cd iosApp -
Initialize the pods:
pod init
This generates the
Podfile. -
In
compose/build.gradle.kts, configure the version, summary, homepage, and baseName of the Podspec file in thecocoapodsblock within thekotlinblock:iosArm64() iosSimulatorArm64() cocoapods { // Required properties // Specify the required Pod version here // Otherwise, the Gradle project version is used version = "1.0.0" summary = "Some description for a Kotlin/Native module" homepage = "Link to a Kotlin/Native module homepage" // Optional properties // Configure the Pod name here instead of changing the Gradle project name name = "Compose" framework { // Required properties // Framework name configuration. Use this property instead of deprecated 'frameworkName' baseName = "Compose" // Optional properties // Specify the framework linking type. It's dynamic by default. isStatic = true // Dependency export // Uncomment and specify another project module if you have one: // export(project(":<your other KMP module>")) //transitiveExport = false // This is default. } // Maps custom Xcode configuration to NativeBuildType //xcodeConfigurationToNativeBuildType["CUSTOM_DEBUG"] = NativeBuildType.DEBUG //xcodeConfigurationToNativeBuildType["CUSTOM_RELEASE"] = NativeBuildType.RELEASE }Then run File | Sync Project with Gradle Files in Android Studio to reimport the project. This process will generate the Podspec file for the project.
-
Update the
Podfileby adding the following line below# Pods for iosApp:pod 'Compose', :path => '../compose'
-
Run the helper script setup_ios.sh. This script automates the following tasks:
./gradlew clean- Create the directory
compose/build/compose/cocoapods/compose-resources ./gradlew :compose:generateDummyFramework(generates a dummy framework inbuild/cocoapods/framework/Compose.framework).- Move to
iosAppand runpod install --repo-update --clean-install. This generates theiosApp.xcworkspacefile, which includes theComposemodule.
-
You will see a notification to reload project as workspace. This is important; the iOS application build will fail if you do not reload.
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
This project uses GitHub Actions for CI/CD. To test the workflows locally without pushing to GitHub, we use act along with a helper script.
Prerequisites:
- Install Docker.
- Install
act(e.g.,brew install act). - Install GitHub CLI
gh(e.g.,brew install gh) for authentication.
How to run: We provide a safe wrapper script to handle artifact paths and permissions automatically.
shell ./run_act_safe.shThis script provides an interactive menu to choose between:
- Main Workflow (Mike Penz): Simulates a
pushevent. Fast, single-job routine for daily feedback. - Dorny Workflow (Manual): Simulates a
workflow_dispatchevent. Multi-job routine for generating detailed standalone reports.
Note: The script will ask for your permission to clean up Gradle build artifacts after execution to save disk space.
Learn more about Kotlin Multiplatform, Compose Multiplatform,


