Kotlin Multiplatform Hybrid Mode: Compose Multiplatform Meets SwiftUI and Jetpack Compose

Introduction
Hello! I'm Yao Xie from the Mobile Application Development Group at KINTO Technologies, where I develop the Android app of KINTO Kantan Moushikomi (KINTO Easy Application). In this article, I’ll be sharing some thoughts on how to implement a hybrid model that takes advantage of both Compose Multiplatform and native UI frameworks.
Developing mobile apps often means juggling limited budgets, varied team sizes, and tight timelines: all while ensuring long-term maintainability and a consistent user experience. A hybrid architecture built on Kotlin Multiplatform (KMP), which now includes Compose Multiplatform as a core UI framework, combined with native frameworks such as SwiftUI (iOS) and Jetpack Compose (Android), offers the flexibility to meet these challenges. Whether you’re launching an MVP on a shoestring budget or scaling up for a fully polished app, this approach adapts to your constraints and preserves code quality over time.
A Flexible and Maintainable Approach for Diverse Constraints
Rapid Development with Consistency
For projects constrained by limited budget, small teams, or short deadlines, the benefits I perceive are the following:
- Shared Business Logic: by writing your core functionality (networking, data models, business rules) once in a KMP module, will ensure consistent behavior across Android and iOS while reducing duplication and ease future maintenance.
- Shared UI with Compose Multiplatform: Compose Multiplatform is part of the Kotlin Multiplatform ecosystem, which means it naturally builds on shared Kotlin code to build UI components. Develop most of your UI as shared composable components. Android benefits directly via Jetpack Compose, and iOS can embed these components using helpers like ComposeUIViewController. This uniform approach guarantees that both platforms adhere to the same design and behavior guidelines, simplifying long-term support.
This strategy minimizes development time while maintaining consistency—a key factor for high-quality user experiences and future-proof code.
"The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License."
Scaling Up with Native Enhancements
As your project secures more funding, personnel, or additional time:
- Incremental Native Integration: With a solid shared foundation, you can gradually replace or enhance key screens using native UI elements. For example, you can enhance critical iOS screens with SwiftUI to better align with platform-specific design standards.
- Expect/Actual Mechanism: Kotlin’s expect/actual pattern lets you define a generic shared component (like a platform-specific button) and then provide native implementations. This enables you to start with a shared UI and later invest in polished native refinements where they matter most.
Adapting to Different Team Sizes and Project Timelines
The hybrid approach is designed to flex as your project evolves:
- For Lean Teams and Tight Schedules: Focus on maximum code reuse. A small team can build and maintain the shared business logic and UI, ensuring consistency while speeding up time-to-market.
- For Larger Teams or Extended Deadlines: As your team grows or deadlines relax, allocate additional resources toward developing native components to enhance user experience. This phased strategy minimizes risk and ensures that the core logic remains stable and maintainable over time.
I find that by keeping the shared logic and UI consistent across platforms, you reduce complexity and technical debt, making projects easier to maintain and evolve.
Code Snippets Illustrating the Kotlin Multiplatform Hybrid Mode
Shared Business Logic
This shared module guarantees that both Android and iOS apps use the same core logic, ensuring consistency and reducing maintenance overhead.
// Api
interface Api {
suspend fun getUserProfile(): User
}
class ApiImpl(private val client: HttpClient) : Api {
override suspend fun getUserProfile(): User {
return client.get {
url {
protocol = URLProtocol.HTTPS
host = "yourdomain.com"
path("yourpath")
}
}.safeBody()
}
}
// Repository
interface UserRepository {
suspend fun getUserProfile(): Flow<User>
}
class UserRepositoryImpl(private val api: Api) : UserRepository {
override suspend fun getUserProfile(): Flow<User> {
return flowOf(api.getUserProfile())
}
}
// ViewModel
class ProfileViewModel(
private val repository: UserRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
fun onAction(action: ProfileScreenAction) {
when (action) {
is ProfileScreenAction.OnInit -> {
viewModelScope.launch {
// Set loading state to true
_uiState.update { it.copy(isLoading = true) }
// Retrieve authorization info and then fetch user profile data
repository.getAuthorizationInfo()
.flatMapLatest { authInfo ->
// If authInfo exists, call getUserProfile() to get user data; otherwise, emit null
if (authInfo != null) {
repository.getUserProfile()
} else {
flowOf(null)
}
}
// Catch any exceptions in the Flow chain
.catch { e ->
e.printStackTrace()
}
// Collect the user profile data and update the UI state
.collect { userProfile ->
_uiState.update { state ->
state.copy(
isLoading = false,
data = userProfile,
errorMessage = if (userProfile == null) "User profile data is null" else null
)
}
}
}
}
}
}
}
Shared UI with Compose Multiplatform
Compose Multiplatform—being an integrated part of Kotlin Multiplatform—allows you to create UI components in shared Kotlin code:
@Composable
fun ProfileScreen(
viewModel: ProfileViewModel = koinViewModel()
) {
val uiState = viewModel.uiState.collectAsState().value
LaunchedEffect(Unit) { viewModel.onAction(ProfileScreenAction.OnInit) }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.background(White)
.padding(16.dp)
) {
// Profile header with image and user details
Row(
modifier = Modifier
.fillMaxWidth()
.background(White, shape = RoundedCornerShape(16.dp))
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = uiState.data.profileImage,
contentDescription = "Profile Image",
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = uiState.data.userName,
fontSize = 16.sp,
color = Color.Black
)
Text(
text = uiState.data.email,
fontSize = 14.sp,
color = Color.Blue.copy(alpha = 0.6f)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
MenuLabel(label = "Account Settings")
....
}
}
Lean Approach: I find using this composable directly on both platforms convenient for a consistent look and behavior.
Scaled Approach: Enhance the UI with native components as needed without altering the underlying business logic.
Native UI via Expect/Actual
Define an expected component:
@Composable
expect fun PlatformButton(
modifier: Modifier = Modifier,
label: String,
onClick: () -> Unit
)
Android Implementation (androidMain):
@Composable
actual fun PlatformButton(
modifier: Modifier,
label: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = modifier
) {
Text(text = label)
}
}
iOS Implementation (iosMain):
@Composable
actual fun PlatformButton(
modifier: Modifier,
label: String,
onClick: () -> Unit
) {
UIKitView(
modifier = modifier,
factory = {
// Create a native UIButton instance.
val button = UIButton.buttonWithType(buttonType = 0)
button.setTitle(label, forState = UIControlStateNormal)
// Style the button
...
// Create a target object to handle button clicks.
val target = object : NSObject() {
@ObjCAction
fun onButtonClicked() {
onClick()
}
}
// Add the target with an action selector.
button.addTarget(
target = target,
action = NSSelectorFromString("onButtonClicked"),
forControlEvents = UIControlEventTouchUpInside
)
button
}
)
}
Blended UI: A Key Advantage Over Flutter
Unlike Flutter, Compose Multiplatform supports blending shared UI and native components seamlessly on a single screen.
In many scenarios, you may not need a complete switch from shared UI to native UI—rather, you can blend them on a single screen. This approach allows you to tap into the strengths of both Compose Multiplatform and native frameworks without fully committing to one for an entire screen. For example, you might want to display a shared composable header alongside a native SwiftUI list on iOS.
Here’s how you can achieve a blended UI:
Embedding Shared Composables in a Native Layout:
One of Compose Multiplatform’s strengths is the ability to seamlessly embed shared Compose UI components into native SwiftUI layouts. Developers can gradually adopt shared components without rewriting entire screens, a flexibility that's difficult to achieve with Flutter’s overlay approach.
For example, you can use SwiftUI as a container to host Compose Multiplatform’s shared UI wrapped in a UIViewController:
import SwiftUI
import shared // Import the shared KMP framework
struct ComposeContainerView: UIViewControllerRepresentable {
let user: User
func makeUIViewController(context: Context) -> UIViewController {
// Use the Compose wrapper to create the UIViewController
return AppComposeUI().createProfileVC(user)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
struct ContentView: View {
let user: User
var body: some View {
VStack {
Text("Native SwiftUI Header")
.font(.headline)
.padding()
// Embed the Compose UI inside SwiftUI.
ComposeContainerView(user: user)
.frame(height: 250)
List {
Text("Native SwiftUI Item 1")
Text("Native SwiftUI Item 2")
}
}
}
}
Embedding Native iOS UI into Compose Multiplatform Screens
Compose Multiplatform also enables embedding native iOS views (e.g., SwiftUI or UIKit) within shared Compose screens—another capability not easily achieved by Flutter.
Define a SwiftUI view to embed:
import SwiftUI
struct MySwiftUIView: View {
let user: User
var body: some View {
VStack {
Text("Native SwiftUI Content")
.font(.body)
Text("Welcome, \(user.firstName)")
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
}
Then integrate this native view into your shared Compose UI using UIKitView:
@Composable
fun ComposeWithNativeUI(user: User) {
Column {
// Shared Compose UI header.
ProfileHeader(user = user)
Spacer(modifier = Modifier.height(16.dp))
// Embed a native iOS view using UIKitView.
UIKitView(factory = {
// Create a UIHostingController with a SwiftUI view.
val hostingController = UIHostingController(rootView = MySwiftUIView(user = user))
hostingController.view
}, modifier = Modifier.fillMaxWidth().height(100.dp))
}
}
This bidirectional inline integration sets Compose Multiplatform apart from Flutter, enabling teams to incrementally adopt shared UI components while maintaining native look and feel. It offers developers greater flexibility and reduces the risk of large-scale rewrites, combining the efficiency of shared code with the richness and quality of native UI.
Compose Multiplatform provides a significant advantage over Flutter by enabling more flexible, fine-grained integration between shared UI and native platform UI. Unlike Flutter, which typically renders shared UI components through an overlay mechanism, Compose Multiplatform uses an inline rendering approach, allowing shared and native UI elements to blend naturally together.
Pros
Best Practices for Flexibility, Maintainability, and Scalability
Embrace Rapid Prototyping:
I’ve learned the hard way that spending weeks perfecting pixel details before users even touch the app is risky. These days our team spins up a shared‑UI prototype in a week or two, pushes it into stakeholders’ hands, and soaks up feedback early. When we’re confident the idea sticks, we layer on native flourishes—animations, platform‑specific gestures—without throwing away what we built.
Plan Incremental Development:
Our rule of thumb: launch small, refactor later. We start with a KMP module for core logic and a single shared Compose screen. Once that slice proves itself, we peel off the next screen—or replace a shared widget with a SwiftUI or Jetpack Compose version—one commit at a time. The gradual approach keeps velocity up and rewrites down.
Balance Resources, Quality, and Scalability:
When it was just two of us, 90 percent of the UI lived in shared code because that’s what we could afford. As head‑count and design ambitions grew, we shifted the critical flows to native, leaving the rest untouched. That staged migration kept the backlog realistic and let us dial quality up without derailing releases.
Maintain Consistency:
Centralizing business logic in one Kotlin module has saved us countless merge headaches. A single bug fix lands once and rolls out everywhere—no more “iOS fixed, Android still broken” stand‑ups. It’s not flashy, but this shared core is what lets the team sleep at night when deadlines loom.
Ensure Elasticity:
In my experience leading mixed‑platform projects, the teams that thrive are the ones that treat architecture like a living organism, not a static blueprint. We design every layer so it can flex when budgets tighten, timelines shift, or a surprise hire brings new skills. Because Compose Multiplatform lets us blend shared and native UI on demand, we can pivot from “MVP mode” to “native‑polish mode” in days, not months. That elasticity has repeatedly saved us when product strategy—or the market itself—changed overnight.
Conclusion
This elastic hybrid architecture, built on Kotlin Multiplatform and Compose Multiplatform alongside native UI frameworks, offers unparalleled flexibility. It allows you to adjust your development strategy based on budget, team size, and timeline constraints while maintaining consistency and long-term maintainability. For lean projects, maximize code reuse to launch quickly; as resources expand, incrementally enhance critical screens with native UI elements without rewriting your core logic.
Embrace this flexible, incremental model to reduce duplication, streamline maintenance, and deliver a consistent, high-quality user experience that scales as your project evolves.
関連記事 | Related Posts

SwiftUI in Compose Multiplatform of KMP

Applying KMP to an Existing App: Our Team’s Experience and Achievements

Mobile App Development Using Kotlin Multiplatform Mobile (KMM) and Compose Multiplatform

Mobile App Development Using Kotlin Multiplatform Mobile (KMM)

SwiftUIをCompose Multiplatformで使用する

Kotlin Multiplatform Mobile(KMM)およびCompose Multiplatformを使用したモバイルアプリケーションの開発
We are hiring!
【プロダクト開発バックエンドエンジニア(リーダークラス)】共通サービス開発G/東京・大阪
共通サービス開発グループについてWebサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がけるグループです。KINTOの名前が付くサービスやKINTOに関わりのあるサービスを同一のユーザーアカウントに対して提供し、より良いユーザー体験を実現できるよう、様々な共通機能や顧客基盤を構築していくことを目的としています。
【UI/UXデザイナー】クリエイティブ室/東京・大阪
クリエイティブ室についてKINTOやトヨタが抱えている課題やサービスの状況に応じて、色々なプロジェクトが発生しそれにクリエイティブ力で応えるグループです。所属しているメンバーはそれぞれ異なる技術や経験を持っているので、クリエイティブの側面からサービスの改善案を出し、周りを巻き込みながらプロジェクトを進めています。