Kotlin: مقدمة في كوتلن متعدد المنصات
آخر تحديث: 2026-08-26
KMP هي الرؤية النهائية لكوتلن — OrderValidator الخاص بـ Charlie يُكتب مرة واحدة ويُعاد استخدامه عبر خلفية JVM وتطبيق iOS وواجهة JS ومتصفح Wasm. آلية expect/actual تبقي اختلافات المنصات فقط عند الحدود.
1. ما ستتعلمه
- بنية KMP:
commonMain/jvmMain/iosMain/jsMain - استراتيجيات الوحدات المشتركة: الشبكات، نماذج البيانات، منطق الأعمال
- تهيئة Gradle متعدد المنصات
- التوافقية: JVM ↔ Java / iOS ↔ Swift / JS ↔ JavaScript
- Charlie في الممارسة:
OrderValidatorمشترك فيcommonMain
2. قصة مهندس معمارية حقيقي
(1) المشكلة: تثليث منطق الأعمال عبر ثلاث منصات
شركة Charlie لديها خلفية JVM، وتطبيق iOS، وواجهة JS. نفس منطق OrderValidator نُفّذ ثلاث مرات بـ Java و Swift و TypeScript. إصلاح خلل واحد يتطلب تغييرات في ثلاثة أماكن — تسبب هذا في حادثتين عدم اتساق سلوك في شهر واحد.
(2) حل الكود المشترك من KMP
KOTLIN
// commonMain: تنفيذ واحد لجميع المنصات
class OrderValidator {
fun validate(order: Order): List<String> {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID")
if (order.total < 0) errors.add("Negative total")
return errors
}
}
قاعدة كود واحدة، ثلاثة منصات. أصلح خللًا مرة واحدة، والسلوك يكون متسقًا بشكل طبيعي في كل مكان.
3. بنية KMP
(1) هيكل المشروع
TEXT
📖 للعرض فقط
shared/
├── src/
│ ├── commonMain/kotlin/ # كود مشترك (جميع المنصات)
│ │ └── com/order/
│ │ ├── Order.kt
│ │ ├── OrderValidator.kt
│ │ └── Platform.kt # تصريحات expect
│ ├── commonTest/kotlin/ # اختبارات مشتركة
│ ├── jvmMain/kotlin/ # كود خاص بـ JVM
│ │ └── com/order/
│ │ └── Platform.kt # actual لـ JVM
│ ├── iosMain/kotlin/ # كود خاص بـ iOS
│ │ └── com/order/
│ │ └── Platform.kt # actual لـ iOS
│ └── jsMain/kotlin/ # كود خاص بـ JS
│ └── com/order/
│ └── Platform.kt # actual لـ JS
└── build.gradle.kts
(2) آلية expect / actual
KOTLIN
// commonMain: صرّح عما تحتاجه (expect)
expect fun getPlatformName(): String
expect class DateFormatter() {
fun format(timestamp: Long): String
}
// jvmMain: قدّم تنفيذ JVM (actual)
actual fun getPlatformName(): String = "JVM"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String =
java.text.SimpleDateFormat("yyyy-MM-dd").format(timestamp)
}
// iosMain: قدّم تنفيذ iOS (actual)
actual fun getPlatformName(): String = "iOS"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String {
// استخدم NSDateFormatter
return NSDateFormatter().apply {
dateFormat = "yyyy-MM-dd"
}.stringFromDate(NSDate(timestamp / 1000.0))
}
}
// jsMain: قدّم تنفيذ JS (actual)
actual fun getPlatformName(): String = "JS"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String {
// استخدم JavaScript Date
return js("new Date(timestamp).toISOString().split('T')[0]")
}
}
(3) بنية KMP متعددة المنصات
flowchart TD
A[commonMain<br/>منطق الأعمال المشترك] --> B[jvmMain<br/>JVM Actual]
A --> C[iosMain<br/>iOS Actual]
A --> D[jsMain<br/>JS Actual]
A --> E[wasmMain<br/>Wasm Actual]
B --> B1[Spring Boot<br/>خلفية]
C --> C1[iOS App<br/>توافق Swift]
D --> D1[Node.js / المتصفح]
E --> E1[وقت تشغيل Web Assembly]
4. تهيئة Gradle متعددة المنصات
(1) build.gradle.kts
KOTLIN
plugins {
kotlin("multiplatform") version "1.9.22"
}
group = "com.order"
version = "1.0.0"
repositories {
mavenCentral()
}
kotlin {
// تعريف المنصات المستهدفة
jvm {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
js(IR) {
browser()
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting
val jvmTest by getting
val iosMain by getting
val jsMain by getting
}
}
5. استراتيجية الوحدات المشتركة
(1) ما يُشارك وما لا يُشارك
| الطبقة | استراتيجية المشاركة | السبب |
|---|---|---|
| نماذج البيانات | ✅ مشاركة كاملة | بيانات بحتة، لا تبعية على المنصة |
| منطق الأعمال | ✅ مشاركة كاملة | القواعد الأساسية يجب أن تكون متسقة |
| الشبكات | ✅ واجهة مشتركة + expect HTTP | الواجهة موحدة، التطبيقات تختلف |
| التسلسل | ✅ مشترك (kotlinx.serialization) | دعم أصلي متعدد التنسيقات |
| واجهة المستخدم | ❌ خاصة بالمنصة | أطر واجهة المستخدم تختلف بشكل كبير |
| قاعدة البيانات | ⚠️ SQL مشترك / expect | SQL قابل للمشاركة، المشغلات تختلف |
| التسجيل | ⚠️ expect/actual | واجهات برمجة التسجيل تختلف حسب المنصة |
(2) أنماط الاستراتيجية المشتركة
KOTLIN
// النمط 1: كود مشترك بحت (لا حاجة لـ expect/actual)
data class Order(val id: String, val total: Double, val status: String)
class OrderValidator {
fun validate(order: Order): List<String> = buildList {
if (!order.id.startsWith("ORD-")) add("Invalid ID format")
if (order.total < 0) add("Negative total")
if (order.status !in validStatuses) add("Invalid status")
}
companion object {
private val validStatuses = setOf("PENDING", "CONFIRMED", "SHIPPED", "CANCELLED")
}
}
// النمط 2: واجهة + مصنع expect
interface HttpClient {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}
expect fun createHttpClient(): HttpClient
// النمط 3: دالة expect للسلوك الخاص بالمنصة
expect fun logDebug(tag: String, message: String)
6. التوافقية
(1) طرق التوافقية بين المنصات
| زوج المنصات | التوافقية | ملاحظات |
|---|---|---|
| JVM ↔ Java | ثنائية الاتجاه سلسة | كوتلن يستدعي Java مباشرة؛ Java يمكنها استدعاء كوتلن |
| iOS ↔ Swift | ثنائية الاتجاه | كوتلن يُترجم إلى إطار Obj-C؛ Swift يستدعي بسلاسة |
| JS ↔ JavaScript | ثنائية الاتجاه | دالة js() تستدعي JS؛ @JsExport تصدّر كوتلن |
| Wasm ↔ JS | اتجاه واحد (JS يستدعي كوتلن) | وحدة Wasm تصدّر دوال لاستدعاءات JS |
(2) مثال توافقية JVM
KOTLIN
// كوتلن يستدعي Java
val order = JavaOrderService() // فئة Java
order.processOrder("ORD-001") // دالة Java
// Java يستدعي كوتلن (البايتكود المُولّد قياسي)
// OrderKt.processOrder(order); // دالة المستوى الأعلى
(3) مثال توافقية JS
KOTLIN
// كوتلن يستدعي JavaScript
fun fetchFromApi(url: String): dynamic {
return js("fetch(url).then(r => r.json())")
}
// تصدير كوتلن إلى JavaScript
@JsExport
class OrderValidator {
fun validate(id: String, total: Double): Boolean {
return id.startsWith("ORD-") && total >= 0
}
}
7. مثال كامل: OrderValidator عبر المنصات
KOTLIN
// ============================================
// OrderProcessor - وحدة KMP المشتركة
// الميزة: OrderValidator مشترك مع تسجيل خاص بالمنصة
// ============================================
// --- commonMain ---
data class Order(val id: String, val total: Double, var status: String, val customer: String)
// expect: تصريح خاص بالمنصة
expect fun logInfo(tag: String, message: String)
class OrderValidator {
fun validate(order: Order): ValidationResult {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID format: ${order.id}")
if (order.total < 0) errors.add("Negative total: ${order.total}")
if (order.total > 1_000_000) errors.add("Total exceeds maximum: ${order.total}")
if (order.status !in VALID_STATUSES) errors.add("Invalid status: ${order.status}")
val result = if (errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(errors)
logInfo("OrderValidator", "Validated ${order.id}: $result")
return result
}
fun calculatePriority(order: Order): String = when {
order.total > 10_000 -> "HIGH"
order.total > 1_000 -> "MEDIUM"
else -> "LOW"
}
companion object {
private val VALID_STATUSES = setOf("PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED")
}
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(val errors: List<String>) : ValidationResult()
}
// --- jvmMain ---
// actual fun logInfo(tag: String, message: String) {
// println("[$tag] $message") // أو استخدم SLF4J
// }
// --- iosMain ---
// actual fun logInfo(tag: String, message: String) {
// NSLog("$tag: $message")
// }
// --- jsMain ---
// actual fun logInfo(tag: String, message: String) {
// console.log("[$tag] $message")
// }
// --- عرض توضيحي (المنصة JVM) ---
fun main() {
// محاكاة actual لـ JVM
// actual fun logInfo(tag: String, message: String) = println("[$tag] $message")
val validator = OrderValidator()
val orders = listOf(
Order("ORD-001", 299.99, "PENDING", "Alice"),
Order("BAD-002", -50.0, "INVALID", "Bob"),
Order("ORD-003", 15_000.00, "CONFIRMED", "Charlie"),
Order("ORD-004", 2_000_000.00, "PENDING", "Dave")
)
println("=== عرض KMP OrderValidator ===")
orders.forEach { order ->
val result = validator.validate(order)
val priority = validator.calculatePriority(order)
when (result) {
is ValidationResult.Valid -> println(" ✅ ${order.id}: صالح (الأولوية: $priority)")
is ValidationResult.Invalid -> println(" ❌ ${order.id}: ${result.errors}")
}
}
}
الإخراج:
TEXT
📖 للعرض فقط
=== عرض KMP OrderValidator ===
✅ ORD-001: صالح (الأولوية: LOW)
❌ BAD-002: [Invalid ID format: BAD-002, Negative total: -50.0, Invalid status: INVALID]
✅ ORD-003: صالح (الأولوية: HIGH)
❌ ORD-004: [Total exceeds maximum: 2000000.0]
8. أمثلة عملية سريعة
▶ مثال: هيكل مشروع KMP
KOTLIN
// هيكل المجلدات
//
// my-kmp-project/
// ├── shared/ # commonMain, commonTest
// │ └── src/
// │ └── commonMain/kotlin/
// │ └── com/example/
// │ ├── Order.kt # مشترك
// │ └── Platform.kt # expect declarations
// ├── androidApp/ # Android target
// ├── iosApp/ # iOS target
// └── desktopApp/ # JVM target
KOTLIN
// shared/src/commonMain/kotlin/com/example/Order.kt
// فئة مشتركة عبر جميع المنصات
data class Order(
val id: String,
val total: Double,
val currency: String = "USD"
)
// مشترك: منطق التحقق
object OrderValidator {
private const val MAX_TOTAL = 1_000_000.0
fun validate(order: Order): Result {
return when {
order.total <= 0 -> Result.Invalid("Total must be positive")
order.total > MAX_TOTAL -> Result.Invalid("Total exceeds maximum: ${order.total}")
else -> Result.Valid(order)
}
}
sealed class Result {
data class Valid(val order: Order) : Result()
data class Invalid(val reason: String) : Result()
}
}
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: expect/actual declarations
KOTLIN
// shared/src/commonMain/kotlin/com/example/Platform.kt
// expect يصرّح بواجهة مشتركة
expect class Platform() {
val name: String
fun getSystemInfo(): String
}
// expect function في object
expect object PlatformContext {
fun currentTimeMillis(): Long
fun log(message: String)
}
// shared/src/androidMain/kotlin/com/example/Platform.kt
actual class Platform actual constructor() {
override val name: String = "Android ${android.os.Build.VERSION.RELEASE}"
override fun getSystemInfo(): String = "Android Device"
}
actual object PlatformContext {
override fun currentTimeMillis(): Long = System.currentTimeMillis()
override fun log(message: String) {
android.util.Log.i("APP", message)
}
}
// shared/src/iosMain/kotlin/com/example/Platform.kt
actual class Platform actual constructor() {
override val name: String = "iOS ${platform.UIKit.UIDevice.currentDevice.systemVersion}"
override fun getSystemInfo(): String = "iPhone/iPad"
}
actual object PlatformContext {
override fun currentTimeMillis(): Long = platform.Foundation.NSDate().timeIntervalSince1970().toLong() * 1000
override fun log(message: String) {
println("[iOS] $message")
}
}
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: مشترك Business Logic
KOTLIN
// منطق مشترك يعمل على جميع المنصات
class OrderRepository {
private val orders = mutableListOf<Order>()
fun add(order: Order) {
require(order.id.isNotBlank()) { "Order ID cannot be blank" }
orders.add(order)
}
fun findById(id: String): Order? = orders.firstOrNull { it.id == id }
fun findHighValue(threshold: Double): List<Order> =
orders.filter { it.total >= threshold }
fun totalRevenue(): Double = orders.sumOf { it.total }
fun byCurrency(): Map<String, Double> =
orders.groupBy { it.currency }
.mapValues { (_, orders) -> orders.sumOf { it.total } }
}
// مشترك: خدمة التحقق والفلترة
class OrderService(private val repo: OrderRepository) {
fun placeOrder(order: Order): OrderValidator.Result {
val result = OrderValidator.validate(order)
if (result is OrderValidator.Result.Valid) {
repo.add(result.order)
}
return result
}
fun getHighValueReport(threshold: Double): String {
val highValue = repo.findHighValue(threshold)
return buildString {
appendLine("High Value Orders Report")
appendLine("========================")
appendLine("Threshold: \$$threshold")
appendLine("Count: ${highValue.size}")
appendLine("Total: \$${highValue.sumOf { it.total }}")
for (order in highValue) {
appendLine("- ${order.id}: \$${order.total} ${order.currency}")
}
}
}
}
// استخدام في commonMain
fun main() {
val repo = OrderRepository()
val service = OrderService(repo)
service.placeOrder(Order("ORD-001", 1500.0))
service.placeOrder(Order("ORD-002", 500.0))
service.placeOrder(Order("ORD-003", 2500.0, "EUR"))
println(service.getHighValueReport(1000.0))
}
**الإخراج:
TEXT
📖 للعرض فقط
High Value Orders Report
========================
Threshold: $1000.0
Count: 2
Total: $4000.0
- ORD-001: $1500.0 USD
- ORD-003: $2500.0 EUR
▶ مثال: Platform-specific tests
KOTLIN
// shared/src/commonTest/kotlin/OrderServiceTest.kt
import kotlin.test.*
class OrderServiceTest {
@Test
fun `placeOrder validates amount`() {
val service = OrderService(OrderRepository())
val result = service.placeOrder(Order("ORD-001", -100.0))
assertTrue(result is OrderValidator.Result.Invalid)
}
@Test
fun `placeOrder adds valid orders`() {
val service = OrderService(OrderRepository())
service.placeOrder(Order("ORD-001", 100.0))
assertNotNull(service.getHighValueReport(50.0))
}
}
// shared/src/androidTest/kotlin/AndroidPlatformTest.kt
// اختبار يعمل فقط على Android
class AndroidPlatformTest {
@Test
fun `platform name includes Android`() {
assertTrue(Platform().name.contains("Android"))
}
}
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: expect functions في interfaces
KOTLIN
// shared/src/commonMain/kotlin/com/example/Storage.kt
expect interface KeyValueStorage {
fun get(key: String): String?
fun put(key: String, value: String)
fun remove(key: String)
fun keys(): Set<String>
}
// مشترك: استخدام
class UserPreferences(private val storage: KeyValueStorage) {
fun saveLastUsername(username: String) {
storage.put("last_username", username)
}
fun getLastUsername(): String? = storage.get("last_username")
fun clear() {
storage.keys().forEach { storage.remove(it) }
}
}
// androidMain: SharedPreferences
actual class AndroidKeyValueStorage(context: Context) : KeyValueStorage {
private val prefs = context.getSharedPreferences("app", Context.MODE_PRIVATE)
override fun get(key: String): String? = prefs.getString(key, null)
override fun put(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
override fun remove(key: String) {
prefs.edit().remove(key).apply()
}
override fun keys(): Set<String> = prefs.all.keys
}
// iosMain: NSUserDefaults
actual class IOSKeyValueStorage : KeyValueStorage {
private val defaults = platform.Foundation.NSUserDefaults.standardUserDefaults
override fun get(key: String): String? = defaults.stringForKey(key)
override fun put(key: String, value: String) {
defaults.setObject(value, forKey = key)
}
override fun remove(key: String) {
defaults.removeObjectForKey(key)
}
override fun keys(): Set<String> = defaults.dictionaryRepresentation().keys
}
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: مكتبة مشتركة لـ Date/Time
KOTLIN
// shared/src/commonMain/kotlin/com/example/DateUtil.kt
expect object DateUtil {
fun now(): Long
fun format(timestamp: Long): String
fun parse(dateString: String): Long?
}
// androidMain
actual object DateUtil {
override fun now(): Long = java.util.Date().time
override fun format(timestamp: Long): String {
val format = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.US)
return format.format(java.util.Date(timestamp))
}
override fun parse(dateString: String): Long? = try {
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.US).parse(dateString)?.time
} catch (e: Exception) {
null
}
}
// iosMain
actual object DateUtil {
override fun now(): Long = platform.Foundation.NSDate().timeIntervalSince1970().toLong() * 1000
override fun format(timestamp: Long): String {
val date = platform.Foundation.NSDate.dateWithTimeIntervalSince1970(timestamp.toDouble() / 1000)
val formatter = platform.Foundation.NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter.stringFromDate(date)
}
override fun parse(dateString: String): Long? = try {
val formatter = platform.Foundation.NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
val date = formatter.dateFromString(dateString) ?: return null
date.timeIntervalSince1970().toLong() * 1000
} catch (e: Exception) {
null
}
}
// الاستخدام في commonMain
fun logOrderEvent(event: String) {
val timestamp = DateUtil.now()
val formatted = DateUtil.format(timestamp)
println("[$formatted] $event")
}
logOrderEvent("Order placed: ORD-001")
**الإخراج:
TEXT
📖 للعرض فقط
[2026-07-27 15:30] Order placed: ORD-001
▶ مثال: Compose Multiplatform
KOTLIN
// shared/src/commonMain/kotlin/com/example/App.kt
// واجهة UI مشتركة (Multiplatform Compose)
@Composable
fun OrderCard(order: Order, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClick() }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Order: ${order.id}", style = MaterialTheme.typography.headlineSmall)
Text("Total: \$${order.total} ${order.currency}", style = MaterialTheme.typography.bodyLarge)
}
}
}
@Composable
fun OrderList(orders: List<Order>, onOrderClick: (Order) -> Unit) {
LazyColumn {
items(orders) { order ->
OrderCard(order = order, onClick = { onOrderClick(order) })
}
}
}
// الاستخدام
@Composable
fun MainScreen() {
val orders = remember { mutableStateListOf(
Order("ORD-001", 1500.0),
Order("ORD-002", 500.0),
Order("ORD-003", 2500.0)
)}
Scaffold(
topBar = { TopAppBar(title = { Text("Order List") }) }
) { padding ->
OrderList(
orders = orders,
onOrderClick = { /* navigation */ }
)
}
}
**الإخراج:
TEXT
📖 للعرض فقط
❓ أسئلة شائعة
س ما الفرق بين KMP و Flutter؟
ج KMP يشارك منطق الأعمال (واجهة مستخدم أصلية لكل منصة)؛ Flutter يشارك واجهة المستخدم (عرض Skia). KMP أكثر مرونة (يحافظ على تجربة واجهة المستخدم الأصلية)؛ Flutter أكثر توحيدًا (واجهة واحدة). يمكنهما التكامل معًا.
س هل KMP جاهز للإنتاج؟
ج منصات JVM و Android مستقرة بالكامل؛ منصة iOS مستقرة؛ منصات JS و Wasm تنضج بسرعة. شركات مثل Netflix و VMware و Cash App تستخدمه بالفعل على نطاق واسع في الإنتاج.
س هل يمكن استخدام expect/actual للفئات؟
ج نعم.
expect class تصرّح بواجهة عبر المنصات؛ actual class تقدّم تطبيقات خاصة بالمنصة. يجب أن تتطابق معلمات المنشئ والدوال تمامًا.س كيف يجب تنظيم الوحدات المشتركة والخاصة بالمنصة؟
ج ضع المنطق المشترك في
commonMain؛ اعزل اختلافات المنصة باستخدام expect/actual عند الحدود. تجنب الكود الخاص بالمنصة في commonMain — استخدم expect لتجريده.س هل KMP صديق لمطوري iOS؟
ج صديق جدًا. KMP يُترجم إلى إطار Obj-C يستدعيه Swift بسلاسة. مطورو iOS يحتاجون فقط للاهتمام بجانب Swift — لا يحتاجون لمعرفة كوتلن.
س كيف هي سرعة البناء لـ KMP؟
ج الترجمة متعددة المنصات تزيد وقت البناء (كل منصة تُترجم بشكل مستقل). الترجمة المتزايدة في Gradle والتخزين المؤقت لمترجم كوتلن تساعد في التخفيف من ذلك. في CI/CD، يُوصى ببناء المنصات بالتوازي.
📖 ملخص
- بنية KMP الأساسية:
commonMain(مشترك) + مجموعات مصادر خاصة بالمنصة (jvmMain، iosMain، jsMain، ...) expectتصرّح بالمتطلبات عبر المنصات؛actualتقدّم تطبيقات خاصة بالمنصة- نماذج البيانات ومنطق الأعمال مشتركة بالكامل؛ واجهة المستخدم وواجهات برمجة المنصات تستخدم
expect/actual kotlin("multiplatform")في Gradle تصرّح ببناءات متعددة المنصات- التوافقية بين المنصات: JVM↔Java سلسة، iOS↔Swift عبر إطار Obj-C، JS↔JS عبر
js()/@JsExport - KMP ليس كُلَّ شيء أو لا شيء — يمكنك اعتماده تدريجيًا بمشاركة وحدات جزئية فقط
📝 تمارين
- مبتدئ (⭐): أنشئ مشروع KMP، عرّف
OrderفيcommonMain، واستخدمه في منصتي JVM و JS. تلميح: إضافة Gradlekotlin("multiplatform") - متوسط (⭐⭐): استخدم
expect/actualلجعلgetPlatformName()تُرجع قيمًا مختلفة على JVM و JS. تلميح:expect fun getPlatformName(): String+ تطبيقاتactual - متقدم (⭐⭐⭐): نفّذ عميل HTTP عبر المنصات: عرّف واجهة
HttpClientفيcommonMain، نفّذ باستخدامjava.net.URLفيjvmMainوبواجهة برمجةfetchفيjsMain. تلميح:expect fun createHttpClient(): HttpClient