کاتلین مولتی‌پلتفرم: راهنمای جامع KMP برای توسعه‌دهندگان اندروید 🚀

چطور با یک کدبیس، هم برای اندروید و هم iOS بنویسیم؟ 🤔

اگه یه توسعه‌دهنده اندروید هستید که دلتون می‌خواد کدهاتون رو با iOS به اشتراک بذارید، یا می‌خواید بدونید KMP دقیقاً چطور کار می‌کنه، این مقاله برای شماست! 🎯

💼 چرا این مقاله رو نوشتیم؟ ما در تیم موبایلت تصمیم گرفتیم پروژه‌مون رو ریفکتور کنیم و قدم به قدم ببریم روی KMP تا بتونیم از یک کدبیس، خروجی‌های مختلف داشته باشیم. این مقاله حاصل تحقیقات و آماده‌سازی ما برای این مهاجرته.

توی این مقاله قراره عمیق بریم زیر پوست Kotlin Multiplatform و ببینیم این تکنولوژی چطور جادوش رو انجام میده - از معماری گرفته تا فرآیند کامپایل.

بزن بریم! 👇


📑 فهرست مطالب

۱. کاتلین مولتی‌پلتفرم (KMP) چیه؟

۲. چرا KMP؟ مقایسه با بقیه راه‌حل‌ها

۳. معماری KMP

۴. ساختار پروژه و Source Set ها

۵. فرآیند کامپایل: از کاتلین تا باینری

۷. کتابخونه‌های مولتی‌پلتفرم

۸. Compose Multiplatform: لایه UI روی KMP

۹. راهنمای مهاجرت


۱. کاتلین مولتی‌پلتفرم (KMP) چیه؟ 🤷‍♂️

کاتلین مولتی‌پلتفرم یه تکنولوژی از جت‌برینز هست که بهتون اجازه میده یک بار کد بنویسید و روی چند پلتفرم اجراش کنید.

پلتفرم‌های پشتیبانی شده 🌍

  • 🤖 اندروید - از طریق Kotlin/JVM

  • 🍎 iOS - از طریق Kotlin/Native

  • 🖥️ دسکتاپ - ویندوز، مک، لینوکس

  • 🌐 وب - از طریق Kotlin/JS و Kotlin/WASM

  • ⚙️ سرور - بک‌اند با Ktor

💡 نکته مهم: KMP فقط برای موبایل نیست! می‌تونید منطق برنامه رو بین اپ موبایل، وب‌سایت و سرور به اشتراک بذارید.

تفاوت KMP با بقیه فریمورک‌ها 🎯

فلسفه KMP: به جای اینکه همه چیز رو از صفر بسازه، KMP بهتون اجازه میده منطق برنامه (business logic) رو به اشتراک بذارید و UI رو بومی نگه دارید.


۲. چرا KMP؟ مزایا و معایب ⚖️

مزایای KMP ✅

۱. اشتراک‌گذاری انتخابی 🎚️ خودتون تصمیم می‌گیرید چقدر کد به اشتراک بذارید. از ۱۰٪ تا ۹۰٪ دست شماست!

۲. UI کاملاً بومی 📱 رابط کاربری اندروید با Jetpack Compose و iOS با SwiftUI یا UIKit. کاربر اصلاً متوجه نمیشه!

۳. دسترسی کامل به API های بومی 🔧 هر وقت لازم باشه می‌تونید مستقیم به API های هر پلتفرم دسترسی داشته باشید.

۴. مهاجرت تدریجی 🚶‍♂️ لازم نیست پروژه رو از صفر بنویسید. می‌تونید کم‌کم کدها رو به KMP منتقل کنید.

۵. پشتیبانی گوگل 🏢 گوگل رسماً از KMP پشتیبانی می‌کنه و کتابخونه‌های Jetpack رو مولتی‌پلتفرم کرده.

معایب KMP ⚠️

  • منحنی یادگیری برای توسعه‌دهنده‌های iOS

  • ابزارها هنوز در حال بالغ شدن هستن

  • سایز باینری iOS ممکنه بزرگ‌تر باشه

  • دیباگ کردن گاهی پیچیده میشه


۳. معماری KMP 🏗️

لایه‌های معماری

┌─────────────────────────────────────────────────────────┐
│                    اپلیکیشن شما                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   ┌─────────────────┐         ┌─────────────────┐      │
│   │  UI اندروید     │         │    UI آی‌او‌اس    │      │
│   │ Jetpack Compose │         │ SwiftUI/UIKit   │      │
│   └────────┬────────┘         └────────┬────────┘      │
│            │                           │               │
│            └───────────┬───────────────┘               │
│                        │                               │
│            ┌───────────▼───────────┐                   │
│            │     Shared Module     │                   │
│            │   (کد مشترک کاتلین)    │                   │
│            │                       │                   │
│            │  • ViewModels         │                   │
│            │  • Repositories       │                   │
│            │  • Use Cases          │                   │
│            │  • Data Models        │                   │
│            │  • Network Layer      │                   │
│            │  • Database           │                   │
│            └───────────────────────┘                   │
│                                                         │
├──────────────────┬──────────────────┬──────────────────┤
│   Kotlin/JVM     │  Kotlin/Native   │   Kotlin/JS      │
│   🤖 اندروید     │    🍎 iOS        │    🌐 وب         │
└──────────────────┴──────────────────┴──────────────────┘

چی رو به اشتراک بذاریم؟ 🤔

۴. ساختار پروژه و Source Set ها 📁

Source Set چیه؟

Source Set روش گریدل برای گروه‌بندی کدها با وابستگی‌های مخصوص خودشونه. توی KMP چند نوع source set داریم:

ساختار یه پروژه KMP 🗂️

my-kmp-project/
│
├── shared/                      # 📦 ماژول مشترک
│   ├── src/
│   │   ├── commonMain/          # 🌍 کد مشترک همه پلتفرم‌ها
│   │   │   └── kotlin/
│   │   │       ├── data/
│   │   │       │   ├── models/
│   │   │       │   ├── repository/
│   │   │       │   └── network/
│   │   │       ├── domain/
│   │   │       │   └── usecase/
│   │   │       └── Platform.kt  # expect declarations
│   │   │
│   │   ├── commonTest/          # 🧪 تست‌های مشترک
│   │   │
│   │   ├── androidMain/         # 🤖 کد مخصوص اندروید
│   │   │   └── kotlin/
│   │   │       └── Platform.android.kt
│   │   │
│   │   ├── iosMain/             # 🍎 کد مخصوص iOS
│   │   │   └── kotlin/
│   │   │       └── Platform.ios.kt
│   │   │
│   │   ├── iosArm64Main/        # 📱 دستگاه واقعی iOS
│   │   ├── iosX64Main/          # 💻 شبیه‌ساز مک اینتل
│   │   └── iosSimulatorArm64Main/  # 💻 شبیه‌ساز Apple Silicon
│   │
│   └── build.gradle.kts
│
├── androidApp/                  # 🤖 اپلیکیشن اندروید
│   ├── src/main/
│   └── build.gradle.kts
│
├── iosApp/                      # 🍎 اپلیکیشن iOS (Xcode)
│   └── iosApp.xcodeproj
│
└── build.gradle.kts

سلسله‌مراتب Source Set ها 🌳

                    commonMain
                        │
           ┌────────────┼────────────┐
           │            │            │
           ▼            ▼            ▼
      androidMain    iosMain      jsMain
                        │
           ┌────────────┼────────────┐
           │            │            │
           ▼            ▼            ▼
      iosArm64Main  iosX64Main  iosSimulatorArm64Main

💡 نکته: می‌تونید source set های میانی هم بسازید. مثلاً mobileMain که بین androidMain و iosMain مشترک باشه!


۵. فرآیند کامپایل: از کاتلین تا باینری 🔬

اینجاست که جادوی واقعی KMP اتفاق میفته! 🪄

کامپایلر کاتلین چطور کار می‌کنه؟

کامپایلر کاتلین یه معماری چند بک‌اندی داره:

                    سورس کاتلین (.kt)
                          │
                          ▼
                ┌─────────────────┐
                │   Frontend      │
                │  (Parser, etc)  │
                └────────┬────────┘
                         │
                         ▼
                ┌─────────────────┐
                │    Kotlin IR    │  ← نمایش میانی مشترک
                │  (Intermediate  │
                │ Representation) │
                └────────┬────────┘
                         │
        ┌────────────────┼────────────────┐
        │                │                │
        ▼                ▼                ▼
   ┌─────────┐     ┌──────────┐    ┌──────────┐
   │ Kotlin/ │     │ Kotlin/  │    │ Kotlin/  │
   │   JVM   │     │  Native  │    │    JS    │
   │ Backend │     │ Backend  │    │ Backend  │
   └────┬────┘     └────┬─────┘    └────┬─────┘
        │               │               │
        ▼               ▼               ▼
   JVM Bytecode    LLVM IR        JavaScript
    (.class)           │            (.js)
        │              ▼
        │         Native Binary
        │         (.framework)
        ▼
   DEX Bytecode
     (.dex)

🤖 مسیر کامپایل اندروید (Kotlin/JVM)

Kotlin Source (.kt)
        │
        ▼ Kotlin Compiler Frontend
   Kotlin IR
        │
        ▼ Kotlin/JVM Backend
 JVM Bytecode (.class)
        │
        ▼ D8/R8 Compiler
  DEX Bytecode (.dex)
        │
        ▼ Android Runtime (ART)
   Native Code

توضیح مراحل:

1️⃣ Kotlin به IR: کامپایلر کاتلین سورس کد رو به یه نمایش میانی (IR) تبدیل می‌کنه.

2️⃣ IR به Bytecode: بک‌اند JVM این IR رو به بایت‌کد جاوا تبدیل می‌کنه.

3️⃣ Bytecode به DEX: کامپایلر D8/R8 بایت‌کد رو به فرمت DEX اندروید تبدیل می‌کنه.

4️⃣ DEX به Native: ران‌تایم ART با AOT و JIT کد بومی تولید می‌کنه.


🍎 مسیر کامپایل iOS (Kotlin/Native)

Kotlin Source (.kt)
        │
        ▼ Kotlin Compiler Frontend
   Kotlin IR
        │
        ▼ Kotlin/Native Backend (Konan)
   LLVM IR
        │
        ▼ LLVM Optimization Passes
   Optimized LLVM IR
        │
        ▼ LLVM Backend
  Native Binary (.framework)
        │
        ▼ Xcode Linker
   iOS App (.ipa)

توضیح مراحل:

1️⃣ Kotlin به IR: مثل اندروید، اول IR تولید میشه.

2️⃣ IR به LLVM IR: بک‌اند Kotlin/Native (با اسم رمز Konan) کد رو به LLVM IR تبدیل می‌کنه.

3️⃣ بهینه‌سازی LLVM: پاس‌های بهینه‌سازی LLVM اعمال میشن (حذف کد مرده، اینلاین کردن، و...).

4️⃣ تولید باینری: LLVM باینری ARM64 تولید می‌کنه.

5️⃣ لینک با Xcode: فریمورک تولید شده با پروژه iOS لینک میشه.


🔥 تفاوت کلیدی

🧠 نکته مهم: iOS هیچ ماشین مجازی نداره! کد کاتلین مستقیم به باینری ARM تبدیل میشه.


۶. سازوکار expect/actual 🎭

این قلب KMP برای مدیریت کدهای مخصوص پلتفرمه!

مفهوم expect/actual

  • expect: یه قرارداد توی commonMain تعریف می‌کنید

  • actual: پیاده‌سازی واقعی توی هر پلتفرم

مثال ساده 📝

// 📁 commonMain/kotlin/Platform.kt
expect class Platform() {
    val name: String
    val version: String
}

expect fun getDeviceId(): String
// 📁 androidMain/kotlin/Platform.android.kt
actual class Platform actual constructor() {
    actual val name: String = "Android"
    actual val version: String = Build.VERSION.SDK_INT.toString()
}

actual fun getDeviceId(): String {
    return Settings.Secure.getString(
        context.contentResolver,
        Settings.Secure.ANDROID_ID
    )
}
// 📁 iosMain/kotlin/Platform.ios.kt
actual class Platform actual constructor() {
    actual val name: String = UIDevice.currentDevice.systemName()
    actual val version: String = UIDevice.currentDevice.systemVersion
}

actual fun getDeviceId(): String {
    return UIDevice.currentDevice.identifierForVendor?.UUIDString ?: ""
}

مثال‌های کاربردی 🛠️

۱. دسترسی به فایل سیستم

// 📁 commonMain
expect fun getAppDirectory(): String

// 📁 androidMain
actual fun getAppDirectory(): String {
    return context.filesDir.absolutePath
}

// 📁 iosMain
actual fun getAppDirectory(): String {
    return NSSearchPathForDirectoriesInDomains(
        NSDocumentDirectory, 
        NSUserDomainMask, 
        true
    ).first() as String
}

۲. کلاینت HTTP

// 📁 commonMain
expect fun createHttpClient(): HttpClient

// 📁 androidMain
actual fun createHttpClient(): HttpClient {
    return HttpClient(OkHttp) {
        install(ContentNegotiation) {
            json()
        }
    }
}

// 📁 iosMain
actual fun createHttpClient(): HttpClient {
    return HttpClient(Darwin) {
        install(ContentNegotiation) {
            json()
        }
    }
}

۳. ذخیره‌سازی امن

// 📁 commonMain
expect class SecureStorage() {
    fun save(key: String, value: String)
    fun get(key: String): String?
    fun delete(key: String)
}

// 📁 androidMain
actual class SecureStorage actual constructor() {
    private val prefs = EncryptedSharedPreferences.create(...)
    
    actual fun save(key: String, value: String) {
        prefs.edit().putString(key, value).apply()
    }
    actual fun get(key: String): String? = prefs.getString(key, null)
    actual fun delete(key: String) = prefs.edit().remove(key).apply()
}

// 📁 iosMain
actual class SecureStorage actual constructor() {
    actual fun save(key: String, value: String) {
        // استفاده از Keychain
    }
    actual fun get(key: String): String? { ... }
    actual fun delete(key: String) { ... }
}

قوانین expect/actual 📋

✅ اسم و پکیج باید یکسان باشه

✅ signature باید دقیقاً match کنه

✅ هر expect باید actual داشته باشه (وگرنه کامپایل خطا میده)

✅ می‌تونید برای کلاس، تابع، پراپرتی، interface و annotation استفاده کنید

💡 نکته طلایی: تا جایی که ممکنه از expect/actual کمتر استفاده کنید. اول دنبال کتابخونه مولتی‌پلتفرم بگردید!


۷. کتابخونه‌های مولتی‌پلتفرم 📚

کتابخونه‌های رسمی و محبوب

کتابخونه‌های Jetpack که مولتی‌پلتفرم شدن 🎉

گوگل داره کتابخونه‌های Jetpack رو مولتی‌پلتفرم می‌کنه:

  • Annotations

  • Collections

  • DataStore

  • Lifecycle

  • ViewModel

  • Room (در حال توسعه)

  • Paging

مثال استفاده از Ktor 🌐

// 📁 commonMain/kotlin/data/network/ApiService.kt

class ApiService(private val client: HttpClient) {
    
    suspend fun getUsers(): List<User> {
        return client.get("https://api.example.com/users").body()
    }
    
    suspend fun getUser(id: Int): User {
        return client.get("https://api.example.com/users/$id").body()
    }
    
    suspend fun createUser(user: User): User {
        return client.post("https://api.example.com/users") {
            contentType(ContentType.Application.Json)
            setBody(user)
        }.body()
    }
}

مثال استفاده از SQLDelight 💾

-- 📁 commonMain/sqldelight/com/example/db/User.sq

CREATE TABLE User (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at INTEGER NOT NULL
);

selectAll:
SELECT * FROM User;

selectById:
SELECT * FROM User WHERE id = ?;

insert:
INSERT INTO User(name, email, created_at) VALUES (?, ?, ?);

deleteById:
DELETE FROM User WHERE id = ?;
// استفاده توی کد
class UserRepository(private val database: AppDatabase) {
    
    fun getAllUsers(): Flow<List<User>> {
        return database.userQueries.selectAll().asFlow().mapToList()
    }
    
    suspend fun insertUser(name: String, email: String) {
        database.userQueries.insert(
            name = name,
            email = email,
            created_at = Clock.System.now().toEpochMilliseconds()
        )
    }
}

۸. Compose Multiplatform: لایه UI روی KMP 🎨

رابطه KMP و CMP

┌─────────────────────────────────────────┐
│       Compose Multiplatform (UI)        │  ← لایه رابط کاربری
├─────────────────────────────────────────┤
│      Kotlin Multiplatform (Logic)       │  ← لایه منطق
└─────────────────────────────────────────┘

KMP: زیرساخت اشتراک‌گذاری کد (منطق برنامه) CMP: امکان اشتراک‌گذاری UI روی این زیرساخت

می‌تونید از KMP بدون CMP استفاده کنید! ✅

┌──────────────────┐    ┌──────────────────┐
│  Jetpack Compose │    │  SwiftUI/UIKit   │
│    (اندروید)     │    │     (iOS)        │
└────────┬─────────┘    └────────┬─────────┘
         │                       │
         └───────────┬───────────┘
                     │
         ┌───────────▼───────────┐
         │   Shared KMP Module   │
         │   (کد مشترک کاتلین)    │
         └───────────────────────┘

این رویکرد خیلی از تیم‌ها ترجیح میدن چون:

  • UI کاملاً بومی می‌مونه

  • تیم iOS می‌تونه با SwiftUI کار کنه

  • ریسک کمتری داره

یا می‌تونید UI رو هم به اشتراک بذارید با CMP 🎨

┌─────────────────────────────────────────┐
│         Compose Multiplatform           │
│          (UI مشترک)                     │
├─────────────────────────────────────────┤
│      Shared KMP Module                  │
│      (منطق مشترک)                       │
└─────────────────────────────────────────┘

CMP چطور روی iOS رندر میشه؟ 🍎

Composable → Compose Runtime → Skiko/Skia → Metal API → صفحه نمایش

Skia کتابخونه گرافیکی گوگله که کروم، فلاتر و اندروید ازش استفاده می‌کنن. CMP از Skia برای رندر کردن UI روی iOS استفاده می‌کنه.


۹. راهنمای مهاجرت 🚀

استراتژی پیشنهادی: گام به گام 🚶‍♂️

📍 مرحله ۱: شروع با یه ماژول کوچیک

با یه چیز ساده شروع کنید:

  • مدل‌های داده

  • کلاس‌های utility

  • ثابت‌ها

// 📁 shared/commonMain/kotlin/models/User.kt

@Serializable
data class User(
    val id: Int,
    val name: String,
    val email: String,
    val avatarUrl: String?
)

📍 مرحله ۲: انتقال لایه شبکه

Retrofit رو با Ktor جایگزین کنید:

// 📁 shared/commonMain/kotlin/data/network/ApiClient.kt

class ApiClient {
    private val client = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                prettyPrint = true
            })
        }
        install(Logging) {
            level = LogLevel.BODY
        }
    }
    
    suspend fun fetchUsers(): List<User> {
        return client.get("https://api.example.com/users").body()
    }
}

📍 مرحله ۳: انتقال لایه دیتابیس

Room رو با SQLDelight جایگزین کنید.

📍 مرحله ۴: انتقال Repository ها

// 📁 shared/commonMain/kotlin/data/repository/UserRepository.kt

class UserRepository(
    private val apiClient: ApiClient,
    private val database: AppDatabase
) {
    fun getUsers(): Flow<List<User>> {
        return database.userQueries.selectAll().asFlow().mapToList()
    }
    
    suspend fun refreshUsers() {
        val users = apiClient.fetchUsers()
        users.forEach { user ->
            database.userQueries.insert(user)
        }
    }
}

📍 مرحله ۵: انتقال ViewModel ها (اختیاری)

// 📁 shared/commonMain/kotlin/presentation/UserListViewModel.kt

class UserListViewModel(
    private val repository: UserRepository
) : ViewModel() {
    
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
    
    fun loadUsers() {
        viewModelScope.launch {
            _isLoading.value = true
            repository.refreshUsers()
            repository.getUsers().collect { _users.value = it }
            _isLoading.value = false
        }
    }
}

📍 مرحله ۶ (اختیاری): اشتراک‌گذاری UI با CMP

اگه خواستید می‌تونید UI رو هم با Compose Multiplatform به اشتراک بذارید.


جدول جایگزینی کتابخونه‌ها 🔄


جمع‌بندی 🎬

کاتلین مولتی‌پلتفرم یه تکنولوژی قدرتمنده که بهتون اجازه میده با یه کدبیس، برای چند پلتفرم توسعه بدید. برخلاف فریمورک‌هایی مثل Flutter که همه چیز رو از صفر می‌سازن، KMP بهتون اجازه میده:

  • ✅ منطق برنامه رو به اشتراک بذارید

  • ✅ UI بومی داشته باشید

  • ✅ گام به گام مهاجرت کنید

  • ✅ به API های بومی دسترسی داشته باشید


🎯 نکات کلیدی

۱. KMP = اشتراک‌گذاری منطق - UI می‌تونه بومی بمونه

۲. کامپایل متفاوت - اندروید از JVM، iOS از LLVM استفاده می‌کنه

۳. expect/actual - راه‌حل KMP برای کد مخصوص پلتفرم

۴. کتابخونه‌های آماده - Ktor، SQLDelight، Koin و...

۵. CMP اختیاریه - می‌تونید فقط از KMP بدون اشتراک‌گذاری UI استفاده کنید

۶. مهاجرت تدریجی - لازم نیست همه چیز رو یهویی عوض کنید


📚 منابع بیشتر

  • 📖 مستندات رسمی https://kotlinlang.org/docs/multiplatform.html

  • 🛠️ ابزار ساخت پروژه: https://kmp.jetbrains.com/

  • 📦 لیست کتابخونه‌ها: https://github.com/terrakok/kmp-awesome

  • 💬 اسلک کاتلین: https://kotlinlang.slack.com/

  • 🎓 کورس رسمی: https://www.jetbrains.com/help/kotlin-multiplatform-dev/


اگه این مقاله براتون مفید بود، لایک کنید و دنبال کنید! 👏

برچسب‌ها: کاتلین، کاتلین مولتی‌پلتفرم، KMP، برنامه‌نویسی اندروید، برنامه‌نویسی آی‌او‌اس، کراس پلتفرم، توسعه موبایل، Kotlin Native، Kotlin Multiplatform