Engineering
Kotlin Multiplatform Shared Business Logic Tutorial: Panduan Lengkap 2026
Pelajari cara berbagi business logic antar platform menggunakan Kotlin Multiplatform (KMP). Tutorial lengkap mencakup arsitektur expect/actual, Ktor untuk networking, SQLDelight untuk database, dan Ko
Pelajari cara berbagi business logic antar platform menggunakan Kotlin Multiplatform (KMP). Tutorial lengkap mencakup arsitektur expect/actual, Ktor untuk networking, SQLDelight untuk database, dan Koin untuk dependency injection.
Apa Itu Kotlin Multiplatform?
Kotlin Multiplatform (KMP) adalah teknologi dari JetBrains yang memungkinkan developer menulis logika bisnis sekali dan menjalankannya di berbagai platform—Android, iOS, web, dan desktop. Berbeda dengan Flutter atau React Native yang mengharuskan menggunakan UI framework tertentu, solusi ini fokus pada berbagi business logic sambil tetap mempertahankan native UI di masing-masing platform.
Pada tahun 2026, teknologi tersebut telah mencapai status production-ready dengan adopsi oleh perusahaan besar seperti Netflix, Airbnb, dan VMware. Google secara resmi mendukung KMP untuk Android, menjadikannya salah satu pendekatan cross-platform paling menjanjikan saat ini. Menurut survei JetBrains 2025, lebih dari 60% developer Kotlin sudah menggunakan atau berencana menggunakan KMP dalam proyek mereka.
Pendekatan ini memecahkan dilema utama dalam cross-platform development: trade-off antara code reuse dan pengalaman pengguna native. Dengan berbagi hanya non-UI code, developer bisa mencapai code reuse hingga 70-80% tanpa mengorbankan native look and feel di setiap platform.
Ekosistem KMP juga semakin matang dengan dukungan library populer seperti Ktor untuk networking, SQLDelight untuk penyimpanan lokal, dan Koin untuk dependency injection. Semua komponen tersebut bekerja secara native di shared module tanpa memerlukan bridge atau wrapper tambahan.
Bagi tim yang ingin mempelajari pendekatan pengembangan modern lainnya, baca juga panduan kami tentang Clean Architecture untuk Android yang membahas pemisahan layer secara mendalam.
Arsitektur KMP: Shared Module Structure
Arsitektur KMP menggunakan konsep source sets untuk memisahkan kode shared dan platform-specific. Struktur standar project KMP terdiri dari beberapa source set utama yang masing-masing memiliki peran spesifik dalam ekosistem aplikasi.
- commonMain — Business logic, data models, dan use cases yang shared ke semua platform target
- androidMain — Implementasi platform-specific untuk Android menggunakan Android SDK
- iosMain — Implementasi platform-specific untuk iOS menggunakan Objective-C interop
- desktopMain — Implementasi untuk desktop menggunakan Java/JVM (opsional)
- wasmJsMain — Implementasi untuk WebAssembly target (eksperimental)
Struktur Gradle Build
Setiap source set dikonfigurasi melalui file build.gradle.kts. Berikut contoh konfigurasi dasar untuk project KMP:
// build.gradle.kts (shared module)
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "Shared"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("app.cash.sqldelight:runtime:2.0.2")
implementation("io.insert-koin:koin-core:4.0.0")
}
}
}
Perhatikan bagaimana dependencies didefinisikan di commonMain. Library seperti Ktor dan SQLDelight akan secara otomatis menggunakan implementasi platform-specific saat di-compile untuk target masing-masing.
Expect/Actual Pattern: Jantung KMP
Pola expect/actual adalah mekanisme utama dalam KMP untuk mengimplementasikan kode platform-specific. Di commonMain, Anda mendeklarasikan expect class atau function, kemudian menyediakan implementasi actual di masing-masing platform.
Mekanisme ini memungkinkan shared module mengakses fungsionalitas platform tanpa mengetahui detail implementasinya. Compiler Kotlin akan memverifikasi bahwa setiap deklarasi expect memiliki pasangan actual di setiap target platform yang dikonfigurasi.
// commonMain - Deklarasi expect
expect class PlatformSpecificHandler() {
fun getPlatformName(): String
fun performPlatformAction()
}
// androidMain - Implementasi actual untuk Android
actual class PlatformSpecificHandler {
actual fun getPlatformName(): String =
"Android ${android.os.Build.VERSION.SDK_INT}"
actual fun performPlatformAction() {
// Android-specific: misalnya akses ke WorkManager
}
}
// iosMain - Implementasi actual untuk iOS
actual class PlatformSpecificHandler {
actual fun getPlatformName(): String =
"iOS ${UIDevice.currentDevice.systemVersion}"
actual fun performPlatformAction() {
// iOS-specific: misalnya akses ke Core Data
}
}
Gunakan expect/actual hanya untuk fungsionalitas yang benar-benar berbeda antar platform. Untuk logika yang bisa di-share sepenuhnya, cukup tulis di commonMain tanpa expect/actual. Pola ini paling cocok untuk akses ke hardware, platform API, dan library native.
Sharing Business Logic dengan Clean Architecture
Pola terbaik untuk proyek KMP adalah menggunakan Clean Architecture dengan pembagian layer yang jelas. Pendekatan ini memastikan business logic tetap terisolasi dari detail implementasi platform, sehingga mudah di-test dan di-maintain.
// Advertisement
Data Layer (Shared)
Data layer bertanggung jawab atas akses data dari berbagai sumber—API remote, database lokal, atau cache. Semua kode di layer ini hidup di commonMain dan menggunakan library cross-platform seperti Ktor serta SQLDelight.
// commonMain - Data Model dengan kotlinx.serialization
@Serializable
data class User(
val id: String,
val name: String,
val email: String,
val createdAt: Long = Clock.System.now().epochSeconds
)
// commonMain - Repository Interface
interface UserRepository {
suspend fun getUsers(): Result<List<User>>
suspend fun getUserById(id: String): Result<User>
suspend fun saveUser(user: User): Result<Unit>
suspend fun deleteUser(id: String): Result<Unit>
}
Domain Layer (Shared)
Domain layer berisi use cases yang merepresentasikan operasi bisnis. Layer ini tidak bergantung pada framework apapun dan bisa di-test secara independen di commonTest.
// commonMain - Use Case
class GetUsersUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(): Result<List<User>> {
return repository.getUsers()
}
}
class GetUserDetailUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(userId: String): Result<User> {
if (userId.isBlank()) {
return Result.failure(
IllegalArgumentException("User ID tidak boleh kosong")
)
}
return repository.getUserById(userId)
}
}
Presentation Layer (Platform-Specific)
Layer presentasi menggunakan framework UI native masing-masing platform. Di Android, Anda bisa menggunakan Jetpack Compose; di iOS, SwiftUI atau UIKit. ViewModel atau Presenter di layer ini memanggil use cases dari domain layer.
// androidMain - ViewModel dengan use case dari shared module
class UserViewModel(
private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
getUsersUseCase()
.onSuccess { users ->
_uiState.value = UserUiState.Success(users)
}
.onFailure { error ->
_uiState.value = UserUiState.Error(error.message)
}
}
}
}
Untuk pembahasan lebih lanjut tentang arsitektur pengembangan mobile, lihat juga artikel kami tentang perbandingan pendekatan cross-platform development 2026.
Ekosistem Library KMP: Ktor, SQLDelight, dan Koin
Keunggulan utama Kotlin Multiplatform terletak pada ekosistem library yang matang dan saling melengkapi. Tiga library utama—Ktor untuk networking, SQLDelight untuk penyimpanan lokal, dan Koin untuk dependency injection—membentuk fondasi yang solid untuk membangun aplikasi cross-platform production-ready.
Networking dengan Ktor Client
Ktor Client adalah HTTP client cross-platform yang memungkinkan network requests dari shared code. Library ini mendukung fitur modern seperti content negotiation, logging, dan retry mechanism secara native di semua platform.
// commonMain - Network Client Setup
val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
})
}
install(Logging) {
level = LogLevel.BODY
logger = object : Logger {
override fun log(message: String) {
println("Ktor: $message")
}
}
}
install(HttpRequestRetry) {
maxRetries = 3
retryOnServerErrors(delayMillis = 1000L)
exponentialDelay()
}
}
// commonMain - API Service Implementation
class ApiService(private val client: HttpClient) {
suspend fun fetchUsers(): List<User> {
return client.get("https://api.example.com/users") {
header("Authorization", "Bearer ${getAuthToken()}")
}.body()
}
suspend fun createUser(user: User): User {
return client.post("https://api.example.com/users") {
contentType(ContentType.Application.Json)
setBody(user)
}.body()
}
suspend fun deleteUser(id: String): Boolean {
val response = client.delete("https://api.example.com/users/$id")
return response.status.isSuccess()
}
}
Konfigurasi di atas berlaku untuk semua platform. Ktor secara otomatis menggunakan engine yang sesuai—OkHttp untuk Android, Darwin untuk iOS, dan CIO untuk desktop atau server-side.
Local Storage dengan SQLDelight
SQLDelight memungkinkan penyimpanan data lokal dengan type-safe SQL queries yang shared ke semua platform. Library ini meng-generate Kotlin code dari file SQL, sehingga Anda mendapatkan compile-time safety untuk semua query database.
-- commonMain - schema.sq
CREATE TABLE User (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0
);
getAllUsers:
SELECT * FROM User ORDER BY name ASC;
getUserById:
SELECT * FROM User WHERE id = ?;
insertUser:
INSERT OR REPLACE INTO User (id, name, email, created_at)
VALUES (?, ?, ?, ?);
deleteUser:
DELETE FROM User WHERE id = ?;
searchUsers:
SELECT * FROM User WHERE name LIKE '%' || ? || '%';
Setelah file SQL di-define, SQLDelight meng-generate class AppDatabase dengan method type-safe untuk setiap query. Berikut implementasi repository yang menggabungkan data dari API dan database lokal:
// commonMain - Repository dengan offline-first strategy
class UserRepositoryImpl(
private val apiService: ApiService,
private val database: AppDatabase
) : UserRepository {
override suspend fun getUsers(): Result<List<User>> {
return try {
val remoteUsers = apiService.fetchUsers()
database.transaction {
remoteUsers.forEach { user ->
database.appDatabaseQueries.insertUser(
user.id, user.name, user.email, user.createdAt
)
}
}
Result.success(remoteUsers)
} catch (e: Exception) {
// Fallback ke data lokal jika network gagal
val localUsers = database.appDatabaseQueries
.getAllUsers()
.executeAsList()
.map { User(it.id, it.name, it.email, it.created_at) }
if (localUsers.isNotEmpty()) {
Result.success(localUsers)
} else {
Result.failure(e)
}
}
}
override suspend fun getUserById(id: String): Result<User> {
val localUser = database.appDatabaseQueries
.getUserById(id)
.executeAsOneOrNull()
return if (localUser != null) {
Result.success(User(localUser.id, localUser.name, localUser.email, localUser.created_at))
} else {
try {
val remoteUser = apiService.fetchUsers().first { it.id == id }
database.appDatabaseQueries.insertUser(
remoteUser.id, remoteUser.name, remoteUser.email, remoteUser.createdAt
)
Result.success(remoteUser)
} catch (e: Exception) {
Result.failure(e)
}
}
}
override suspend fun saveUser(user: User): Result<Unit> {
return try {
apiService.createUser(user)
database.appDatabaseQueries.insertUser(
user.id, user.name, user.email, user.createdAt
)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun deleteUser(id: String): Result<Unit> {
return try {
apiService.deleteUser(id)
database.appDatabaseQueries.deleteUser(id)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// Advertisement
Dependency Injection dengan Koin
Koin adalah dependency injection framework yang mendukung KMP secara native. Library ini menggunakan pendekatan DSL (Domain-Specific Language) yang ringan dan tidak memerlukan code generation—sangat cocok untuk shared module di KMP.
// commonMain - Module Definition
val sharedModule = module {
// Networking
single {
HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
}
single { ApiService(get()) }
// Database
single { createAppDatabase(get()) }
// Repository
single<UserRepository> { UserRepositoryImpl(get(), get()) }
// Use Cases
factory { GetUsersUseCase(get()) }
factory { GetUserDetailUseCase(get()) }
}
// commonMain - Koin Initialization
fun initKoin(): KoinApplication {
return startKoin {
modules(sharedModule)
}
}
Setiap platform kemudian menginisialisasi Koin dengan cara yang sesuai:
// androidMain - Android Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
// Android-specific modules bisa ditambahkan di sini
}
}
// iosMain - iOS App Entry Point
fun initKoinIOS() {
initKoin()
// iOS-specific modules bisa ditambahkan di sini
}
Keunggulan Koin dibanding Hilt atau Dagger adalah tidak adanya proses annotation processing, sehingga build time tetap cepat meskipun project KMP memiliki banyak target platform. Untuk perbandingan lebih detail tentang dependency injection, baca artikel kami tentang dependency injection di Android modern.
Tiga library di atas—Ktor, SQLDelight, dan Koin—sudah terbukti di production oleh perusahaan seperti Netflix dan Airbnb. Kombinasi ketiganya membentuk stack yang lengkap untuk hampir semua kebutuhan pengembangan aplikasi cross-platform.
Best Practices untuk KMP di Production
Menerapkan KMP di production memerlukan perhatian khusus terhadap beberapa aspek teknis. Berikut rekomendasi berdasarkan pengalaman perusahaan yang sudah mengadopsi teknologi ini secara luas:
Matriks Rekomendasi
| Area | Metrik / Target | Tools yang Direkomendasikan |
|---|---|---|
| Code Sharing Ratio | Targetkan 70-85% kode di commonMain | Gradle source set analysis |
| Unit Test Coverage | Minimal 80% untuk shared module | Kotlin Test, kotlinx-coroutines-test |
| Build Time | < 3 menit untuk full build | Gradle build cache, configuration cache |
| APK Size Impact | Tambahan < 2 MB dari shared module | R8/ProGuard, KMP binary stripping |
| Memory Overhead | < 5 MB untuk shared runtime | New Native memory model (default sejak Kotlin 2.0) |
| Startup Time | Tambahan < 100ms dari shared init | Lazy Koin initialization |
Strategi Migrasi Bertahap
Jangan langsung memigrasikan seluruh codebase ke KMP. Gunakan pendekatan bertahap yang dimulai dari layer data:
- Fase 1 (Minggu 1-2): Setup shared module dengan data models dan repository interface. Buat struktur Gradle yang benar sebelum menulis kode apapun.
- Fase 2 (Minggu 3-4): Implementasikan networking layer dengan Ktor dan local storage dengan SQLDelight. Pastikan kedua layer bisa di-test secara independen.
- Fase 3 (Minggu 5-6): Pindahkan use cases ke domain layer di shared module. Refactor ViewModel untuk memanggil use cases dari shared.
- Fase 4 (Minggu 7-8): Setup CI/CD pipeline untuk build multi-platform. Tambahkan unit tests dan integration tests di commonTest.
Error Handling yang Konsisten
Gunakan Result type atau sealed class untuk error handling yang konsisten di semua platform. Hindari melempar exception dari shared module—gunakan pattern matching di layer presentasi untuk menangani berbagai jenis error.
// commonMain - Sealed class untuk error handling
sealed class AppError(val message: String) {
data class Network(val code: Int, val detail: String) :
AppError("Network error $code: $detail")
data class Database(val detail: String) :
AppError("Database error: $detail")
data class Validation(val field: String, val rule: String) :
AppError("Validation error on $field: $rule")
data class Unknown(val throwable: Throwable) :
AppError("Unknown error: ${throwable.message}")
}
// Extension function untuk mapping
fun Exception.toAppError(): AppError = when (this) {
is ResponseException -> AppError.Network(
response.status.value, message ?: "Unknown"
)
is SQLException -> AppError.Database(message ?: "DB Error")
is IllegalArgumentException -> AppError.Validation("input", message ?: "")
else -> AppError.Unknown(this)
}
Kesimpulan dan Langkah Selanjutnya
Kotlin Multiplatform pada tahun 2026 telah menjadi solusi yang matang dan production-ready untuk pengembangan lintas platform. Dengan berbagi business logic melalui shared module, tim developer bisa menghemat waktu development hingga 40-60% sambil tetap mempertahankan native UI experience di setiap platform.
Kombinasi Ktor untuk networking, SQLDelight untuk local storage, Koin untuk dependency injection, dan kotlinx.serialization untuk data serialization membuat ekosistem KMP sangat lengkap. Semua komponen tersebut memiliki dokumentasi yang baik dan komunitas aktif yang terus berkembang.
Untuk memulai, gunakan template resmi dari JetBrains atau ikuti tutorial resmi di dokumentasi Kotlin Multiplatform. Mulai dari sharing data layer dan domain layer, lalu evaluasi apakah Compose Multiplatform sesuai untuk kebutuhan UI Anda.
Untuk memulai dengan KMP, kunjungi Kotlin Multiplatform Documentation atau gunakan template project dari KMP App Template.
Pertanyaan yang Sering Diajukan (FAQ)
commonTest dengan framework seperti kotlin.test. Unit tests di commonTest akan dijalankan di semua platform target. Untuk integration testing, gunakan library seperti Turbine untuk testing Flow dan coroutine-based logic.Sumber Referensi
// Advertisement
VyuApp Studio
Bespoke web engineering — Garut, ID