امکانات چند پلتفرمی کاتلین برای اشتراک کد بین اندروید و iOS — راهنمای کاربردی
در این مقاله قصد داریم در خصوص اشتراک کد بین اندروید و iOS با استفاده از امکانات چند پلتفرمی کاتلین صحبت کنیم. برای مطالعه این راهنما لزومی ندارد حتماً دانش توسعه اندروید داشته یا آموزش برنامه نویسی iOS دیده باشید، اما اگر چنین دانشی داشته باشید، سریعتر موضوع را درک خواهید کرد.
امکانات چند پلتفرمی کاتلین
اپلیکیشنهای اندروید و iOS به طور معمول زمانی که آنها را به کارکردهایشان تجزیه کنیم، ماهیت یکسانی مییابند، اما در هر حال مجبور هستیم آنها را در زبانها و با ابزارهای مختلفی بنویسیم تا بتوانیم روی پلتفرم مربوطه اجرا کنیم. برای حل این مشکل فناوریهای چند پلتفرمی مختلفی مانند React Native و Flutter معرفی شدهاند که دو مورد از مهمترین فریمورکهای چند پلتفرمی مهم محسوب میشوند. اما کاتلین به عنوان یک تازهوارد در حال یافتن مسیر خود است.
فریمورکهای موجود که برشمردیم همگی خوب هستند، شکی نیست که کارشان را به خوبی انجام میدهند اما با این حل شما را ملزم میسازند که همه کدهای موجود را بازنویسی کرده و به دنیای آنها ببرید. بدین ترتیب میبایست مهندسان مجدداً آموزش ببینند و سپس با فریمورک جدید مأنوس شوند. ضمناً فریمورک جدید صرفاً یک پل به دنیای نیتیو محسوب میشود. آنها صرفاً کار را به جای شما انجام میدهند، اگر بخواهید کاری را در سطح نیتیو انجام دهید، قادر نخواهید بود، زیرا در اغلب موارد محدود به آن چیزی هستید که فریمورک در اختیارتان قرار میدهد. این همان جایی است که کاتلین وارد میشود.
امکان چند پلتفرمی کاتلین به منزله ورود Jetbrain به دنیای چند پلتفرمی است. به این ترتیب به جای انتقال به فریمورک دیگر، کافی است آن چه را برای هماهنگ شدن با پلتفرمهای دیگر نیاز دارید به اشترک بگذارید. مهندسان همچنان به انجام کارهای خود میپردازند و گرچه باید کمی دانش جدید اخذ کنند، اما لزومی وجود ندارد که چیزی را از صفر بیاموزند. بدین ترتیب میتوانید منطق شبکهبندی، منطق کَش کردن، منطق تجاری و منطق اپلیکیشن را بر اساس نیازهای خود به اشتراک بگذارید. برخی افراد صرفاً لایه شبکه را به اشتراک میگذارند. امکان پیکربندی آن برحسب کاربرد موردی وجود دارد، اما در این مقاله شیوه اشتراکگذاری همه آنها را مورد بررسی قرار میدهیم.
طرز کار کاتلین چند پلتفرمی چگونه است؟
کاتلین هدفهای متفاوتی را کامپایل میکند و به این ترتیب میتواند به صورت خروجیهای مختلفی برای پلتفرمهای متفاوت کامپایل شود.
- Kotlin/JVM خروجیهایی به صورت فایلهای JAR/AAR ارائه میکند که از سوی پروژههای جاوا مانند اندروید و Spring Boot مورد استفاده قرار میگیرد.
- Kotlin/JS فایلهای JS تولید میکند که امکان استفاده از در پروژههای جاوا اسکریپت وجود دارد. بدین ترتیب کاتلین میتواند برای فریمورکهایی مانند React و Node مورد استفاده قرار گیرد.
- Kotlin/Native فایلهای باینری ارائه میکند که امکان استفاده پلتفرمهای نیتیو از آن را فراهم میسازد. همچنین میتواند فریمورکهای اپل را در خروجی ارائه کند که موجب میشود روی دستگاههای اپل مانند iOS و macOS قابل استفاده باشد و یا فایلهای اجرایی برای هدفهای نیتیو دیگر مانند ویندوز و لینوکس عرضه میکند.
با توجه به این که کاتلین به این هدفها کامپایل میشود، میتوانیم کد کاتلین را یک بار بنویسیم و آن کد را برای هدف خاصی که نیاز داریم کامپایل کنیم و خروجی صحیحی برای استفاده روی پلتفرم مقصد به دست آوریم.
expect/actual
در اغلب موارد صرفاً کدی مینویسیم و به کاتلین اجازه میدهیم که آن را به هدف مورد نظر کامپایل کند، اما اگر چیزی باشد که کاتلین نشناسد چطور؟ فرض کنید میخواهید مقداری را روی اپلیکیشنتان ذخیره کنید. در اندروید میتوانید این کار را با استفاده از SharedPreferences انجام دهید. روی iOS این کار با استفاده از NSUserDefaults انجام میشود. کاتلین به صورت پیشفرض این موضوع را نمیداند. کاتلین تنها میداند که کد کاتلین به پلتفرمهای مختلفی کامپایل میشود، اما میتوانید با استفاده از مکانیسم expect/actual کاتلین را از این موضوع آگاه سازید.
Expect به کاتلین اعلام میکند که موردی وجود دارد که میتواند انجام دهد، اما نمیداند چطور انجام دهد، و صرفاً پلتفرم مقصد شیوه اجرای آن را میداند. actual نیز صرفاً اعلان پلتفرم در مورد شیوه انجام آن کار است. مثال کد به صورت زیر است:
1// Common Code
2expect fun saveValueLocally(value: String)
3
4// Android Code
5actual fun saveValueLocally(value: String) {
6 val sharedPreferences = …
7 sharedPreferences.edit { putString("MyString", value) }
8}
9
10// iOS Code
11actual fun saveValueLocally(value: String) {
12 NSUserDefaults.standardUserDefaults.setValue(
13 value,
14 forKey = "String"
15 )
16}
اکنون میتوانید از saveValueLocally استفاده کنید تا کاتلین بداند که باید روی اندروید از NSUserDefaults و روی iOS از SharedPreferences استفاده کند. این کار را میتوان در مورد هر چیزی که در پلتفرمهای مختلف، متفاوت است، مانند Data اجرا کرد.
چه چیزی را میتوانیم به اشتراک بگذاریم؟
برای بیشینهسازی اشتراک کد بین اندروید و iOS، همه چیزهایی را که امکان اشتراک دارند به اشتراک میگذاریم. این موارد شامل لایه داده برای شبکهبندی و کَش کردن (Cashing)، لایه دامنه (Domain) برای منطق تجاری و بخشی از لایه ارائه است که میتواند شامل منطق اپلیکیشن باشد. در حالت کلی لایه ارائه را به اشترک نمیگذاریم، زیرا به پلتفرمهای مختلف وابسته است. برای نمونه در اندروید میتواند شامل Activity / Fragment و در iOS شامل ViewController باشد. این چیزی است که واقعاً امکان اشتراک ندارد و در پلتفرمهای مختلف کاملاً متفاوت است.
راهاندازی کاتلین چند پلتفرمی
نخستین کاری که برای راهاندازی کاتلین چند پلتفرمی باید انجام دهیم، تنظیم مواردی برای اشتراک کد است. یک ماژول Gradle با هر نامی ایجاد کنید (در این مثال از SharedCode استفاده کردهایم) و هدفهای آن را به کاتلین اعلام کنید. پیکربندی ساده برای ماژول کد اشتراکی چنین است:
1plugins {
2 id("com.android.library")
3 id("org.jetbrains.kotlin.multiplatform")
4}
5
6kotlin {
7 ios()
8 android()
9
10 sourceSets["commonMain"].dependencies {
11 implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
12 }
13 sourceSets["iosMain"].dependencies {
14 implementation("org.jetbrains.kotlin:kotlin-stdlib")
15 }
16}
17
18android {
19 sourceSets {
20 getByName("main") {
21 manifest.srcFile("src/androidMain/AndroidManifest.xml")
22 java.srcDirs("src/androidMain/kotlin")
23 res.srcDirs("src/androidMain/res")
24 }
25 }
26}
27
28dependencies {
29 implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
30}
بلوک plugins اعلام میکند که این یک کتابخانه اندروید و یک پروژه چند پلتفرمی است. بدین ترتیب میتوانیم هم Multiplatform و هم Android را پیکربندی کنیم. درون بلوک kotlin هدفهای خود را تعیین میکنیم. در این مثال ios و android ذکر شدهاند. به این ترتیب هدفهایی ایجاد میشوند که میتوان در ادامه آنها را بیشتر پیکربندی کرد.
همچنین میتوانیم وابستگیهایی به هدفها اضافه کنیم. در این قطعه کد ما صرفاً کتابخانه کاتلین را اضافه کردهایم. آنها را به همه هدفها اضافه کردیم تا کاتلین بداند که آن را روی هر هدف چطور کامپایل کند. به بلوک android توجه کنید. این بلوک طوری پیکربندی شده است که نام پیشفرض main به androidMain تغییر یابد تا نام پوشه معنی بیشتری داشته باشد. این پیکربندی موجب میشود که پروژه ساختار زیر را داشته باشد:
1├── build.gradle.kts
2├── src
3| ├── androidMain
4| | ├── AndroidManifest.xml
5| | ├── res
6| | └── kotlin
7| ├── iosMain
8| | └── kotlin
9| └── commonMain
10| └── kotlin
11└── etc
commonMain جایی است که کد اشتراکی را قرار میدهیم و androidMain و iosMain جایی است که کد پلتفرم در صورت نیاز در آن جای میگیرد. اکنون میتوانیم شروع به نوشتن کد بکنیم.
اشتراک کد روی لایه داده
این لایه میتواند شامل هر چیزی باشد که با دادهها کار میکند. لایه Data جایی است که دادهها را برای اپلیکیشن خود دریافت و یا ذخیره میکنیم. برای این که این مقاله ساده بماند، دادهها را از منبع ریموت دریافت میکنیم.
شبکهبندی
خوشبختانه هم اینک کتابخانههای چند پلتفرمی برای شبکهبندی وجود دارند و از این جهت میتوانیم صرفاً از Ktor (+) به عنوان کلاینت HTTP خود، از Kotlin serialization (+) بری تجزیه JSON و از Kotlin Coroutines (+) برای مدیریت وظایف ناهمگام استفاده کنیم. برای آشنایی بیشتر با این کتابخانهها لطفاً با مراجعه به لینکهای فوق در مورد هر یک مطالعه کنید. ابتدا باید وابستگیها را در پیکربندی Gradle اضافه کنیم:
1kotlin {
2 …
3 sourceSets["commonMain"].dependencies {
4 …
5 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.3")
6 implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:0.14.0")
7 implementation("io.ktor:ktor-client-core:1.2.6")
8 implementation("io.ktor:ktor-client-json:1.2.6")
9 implementation("io.ktor:ktor-client-serialization:1.2.6")
10 implementation("io.ktor:ktor-client-ios:1.2.6")
11 }
12
13 sourceSets["iosMain"].dependencies {
14 …
15 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3")
16 implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:0.14.0")
17 implementation("io.ktor:ktor-client-ios:1.2.6")
18 implementation("io.ktor:ktor-client-json-native:1.2.6")
19 implementation("io.ktor:ktor-client-serialization-native:1.2.6")
20 }
21}
22
23dependencies {
24 …
25 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3")
26 implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0")
27 implementation("io.ktor:ktor-client-android:1.2.6")
28 implementation("io.ktor:ktor-client-json-jvm:1.2.6")
29 implementation("io.ktor:ktor-client-serialization-vm:1.2.6")
30}
در گام بعدی باید HttpClientEngine را برای هر پلتفرم تعیین کنیم:
1// commonMain
2expect val engine: HttpClientEngine
3
4// androidMain
5actual val engine by lazy { Android.create() }
6
7// iosMain
8actual val engine by lazy { Ios.create() }
اینک میتوانیم یک ItemRepository ایجاد کنیم که از Ktor برای اجرای درخواست شبکه و دریافت برخی دادهها بهره میگیرد:
1class ItemRepository {
2 private val client = HttpClient(engine) {
3 install(JsonFeature) {
4 serializer = KotlinxSerializer().apply {
5 register(Item.serializer().list)
6 }
7 }
8 }
9
10 suspend fun getItems(): List<Item> =
11 client.get("https://url.only.fortest/items")
12}
متغیر client به مقداردهی HttpClient بر مبنای موتور مورد استفاده (اندروید/iOS) میپردازد. همچنین از آن استفاده میکنیم تا بتوانیم JSON را با استفاده از KotlinxSerializer تجزیه کنیم و سریالایزر را برای آیتم خود ثبت نماییم. در این خصوص در ادامه بیشتر توضیح میدهیم. بدین ترتیب Ktor شیوه تجزیه یک آیتم را از یک رشته JSON میداند. پس از این که این موارد را راهاندازی کردیم، اینک صرفاً میتوانیم از کلاینت استفاده و درخواستهایی مانند client.get، client.post و غیره از طریق آن اجرا کنیم. بدین ترتیب کد شبکه به اشتراک گذاشته شد. هم اکنون میتوانیم از این کد در هر دو پلتفرم اندروید و iOS بهره بگیریم.
اشتراک روی لایه دامنه
در این بخش منطق تجاری را در اپلیکیشن خود قرار میدهیم. در این مثال کل مدل را قرار خواهیم داد.
1@Serializable
2data class Item(val value: String)
در کد فوق صرفاً مدل دادهها برای موجودیت را به اشتراک میگذاریم. ضمناً به حاشیهنویسی Serializable@ توجه کنید. بدین ترتیب کلاس میتواند از JSON و به آن serialize و deserialize شود.
اشتراک روی لایه ارائه
لایه ارائه جایی است که منطق اپلیکیشن را کنترل میکنیم. بدین ترتیب آن چه ارائه خواهد شد و همچنین ورودیها و تعاملهای کاربر مدیریت میشوند. ViewModel-ها را میتوانیم در این جا به اشترک بگذاریم.
برای شروع یک BaseViewModel ایجاد میکنیم که از کامپوننتهای معماری روی اندروید استفاده میکند و روی iOS صرفاً یک ViewModel است:
1// commonMain
2expect open class BaseViewModel() {
3 val clientScope: CoroutineScope
4 protected open fun onCleared()
5}
6
7// androidMain
8actual open class BaseViewModel actual constructor(): ViewModel() {
9 actual val clientScope: CoroutineScope = viewModelScope
10 actual override fun onCleared() {
11 super.onCleared()
12 }
13}
14
15// iosMain
16actual open class BaseViewModel actual constructor() {
17 private val viewModelJob = SupervisorJob()
18 val viewModelScope: CoroutineScope = CoroutineScope(IosMainDispatcher + viewModelJob)
19
20 actual val clientScope: CoroutineScope = viewModelScope
21
22 protected actual open fun onCleared() {
23 viewModelJob.cancelChildren()
24 }
25
26 object IosMainDispatcher : CoroutineDispatcher() {
27 override fun dispatch(context: CoroutineContext, block: Runnable) {
28 dispatch_async(dispatch_get_main_queue()) { block.run() }
29 }
30 }
31}
اندروید از قبل ابزارهایی دارد که از طریق کامپوننتهای Architecture ساخته شدهاند، لذا از آنها استفاده میکنیم. iOS چنین ابزاری ندارد و از این رو آنها را ایجاد میکنیم. خوشبختانه کار چندان دشواری نیست. برای این که BaseViewModel بتوانید تغییرات دادهها را به view انتشار دهید، میتوانید از Flow کوروتین استفاده کنید.
«تابعهای تعلیقی» (Suspending functions) به ObjC کامپایل نمیشوند و از این رو نمیتوانیم از آنها روی iOS بهره بگیریم، اما به لطف CFlow از KotlinConf میتوانیم این کار را انجام دهیم. سورس کد آن در این ریپو (+) ارائه شده است.
1<T> ConflatedBroadcastChannel<T>.wrap(): CFlow<T> = CFlow(asFlow())
2
3fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)
4
5class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
6 fun watch(block: (T) -> Unit): Closeable {
7 val job = Job(/*ConferenceService.coroutineContext[Job]*/)
8
9 onEach {
10 block(it)
11 }.launchIn(CoroutineScope(dispatcher() + job))
12
13 return object : Closeable {
14 override fun close() {
15 job.cancel()
16 }
17 }
18 }
19}
CFlow اساساً پوششی برای Flow است و یک تابع watch معمولی عرضه میکند، از این رو میتوانیم آنها را مشاهده کنیم و یک لامبدا به عنوان مقایسه برای استفاده از تابع تعلیقی با Flow ارسال نماییم. همچنین تابعهای کمکی برای تبدیل Flow محسوب میشوند و ConflatedBroadcastChannel را به CFlow بدیل میکنند. در ادامه فایلی به نام FlowUtils.kt ساخته و به پروژه اضافه میکنیم. watch از ژنریکها استفاده میکند. برای فعالسازی ژنریکها در کد سوئیفت باید برخی پیکربندیها را ایجاد کنیم:
فایل SharedCode-build.gradle.kts
1ios() {
2 compilations {
3 val main by getting {
4 kotlinOptions.freeCompilerArgs = listOf("-Xobjc-generics")
5 }
6 }
7}
فایل Sample.kt
1private val _dataToPropagate = ConflatedBroadcastChannel<String>()
2val dataToPropagate = _dataToPropagate.wrap()
3
4fun someFunction() {
5 _dataToPropagate.offer("The Data")
6}
در قطعه کد فوق از ConflatedBroadcastChannel و Flow بری ارائه دادهها به مصرفکنندگان مدل View استفاده کردهایم. از یک ConflatedBroadcastChannel برای انجام این کار استفاده میکنیم تا جدیدترین مقداری که ویوهایمان نیاز دارد را ذخیره سازد. از نظر توسعهدهندگان اندروید، رابطه زیر برقرار است:
ConflatedBroadcastChannel = MutableLiveData Flow = LiveData
با استفاده از ابزارهای فوق میتوانیم شروع به ساخت مدلهای View خود بکنیم. فرض کنید میخواهیم لیستی از آیتمها را ببینیم.
1class ViewItemsViewModel(
2 private val itemsRepository: ItemsRepository
3) : BaseViewModel() {
4 private val _items = ConflatedBroadcastChannel<String>()
5 val items = _items.wrap()
6
7 init {
8 clientScope.launch {
9 _items.offer(itemsRepository.getItems())
10 }
11 }
12
13 @ThreadLocal
14 companion object {
15 fun create() = ViewItemsViewModel(ItemsRepository())
16 }
17}
همچنین یک تابع کمکی create برای ایجاد یک ViewModel اضافه کردهایم. ThreadLocal به ما کمک میکند که با مدل همزمانی Kotlin/Native کار کنیم. اندروید نیز به یک factory برای ViewModel-های خود نیاز دارد تا بخش کش کردن کار کند. بنابراین آن را ایجاد میکنیم.
1// androidMain
2
3class ViewItemsViewModelFactory : ViewModelProvider.Factory {
4 override fun <T : ViewModel?> create(modelClass: Class<T>): T {
5 return ViewItemsViewModel.create() as T
6 }
7}
اکنون یک مدل view اشتراکی داریم. تنها کاری که باقی مانده است مصرف کردن آن روی پروژههای اندروید و iOS است.
استفاده از کد اشتراکی در اندروید
خوشبختانه استفاده از کد اشتراکی در اندروید کاملاً آسان است، زیرا یک پروژه Gradle نیز محسوب میشود. کافی است آن را به عنوان یک وابستگی در پیکربندی Gradle پروژه اندروید اضافه کنید.
1dependencies {
2 …
3 implementation(project(":SharedCode"))
4}
بدین ترتیب میتوانیم از آن در یک Fragment استفاده کنیم.
1class ViewItemsFragment : Fragment(R.layout.fragment_view_items) {
2 private val factory = ViewItemsViewModelFactory()
3 private val viewModel by viewModels<ViewItemsViewModel> { factory }
4
5 override fun onCreate(…) {
6 viewModel.items.watch {
7 // Refresh the RecyclerView contents
8 }
9 }
10}
اگر کد CFlow را بررسی کرده باشید، میبینید که watch یک Closeable بازمیگرداند. دلیل این مسئله آن است که به خاطر بسپاریم آن را در آینده پاک کنیم تا از نشت حافظه جلوگیری شود. این آیتم شباهت زیادی به Disposable در RxJava دارد. یک ارجاع به هر یک از آنها نگه میداریم و در ادامه آنها را close میکنیم و یا احتمالاً چیزی ایجاد میکنیم تا به این کار کمک کند.
استفاده از کد اشتراکی در iOS
در مورد iOS نیاز به کار بیشتری وجود دارد. باید یک فریمورک برای خروجی آن ایجاد کنیم و آن را روی Xcode مصرف کنیم. برای این که کارها آسانتر شود از cocoapods برای مدیریت تنظیمات استفاده میکنیم. در پیکربندی Gradle ماژول کد اشتراکی به صورت زیر عمل میکنیم:
1plugins {
2 …
3 id("org.jetbrains.kotlin.native.cocoapods")
4}
5
6version = "1.0.0"
7
8kotlin {
9 cocoapods {
10 summary = "Shared Code for Android and iOS"
11 homepage = "Link to a Kotlin/Native module homepage"
12 }
13}
افزودن این پیکربندی موجب اضافه شدن podspec میشود که یک فایل podspec ایجاد میکند و میتوانید در پروژه iOS به آن ارجاع بدهید. برای استفاده از cocoapods در iOS به این لینک (+) رجوع کنید. وظیفه podspec را از طریق دستور زیر اجرا کنید تا فایلی به دست آید:
./gradlew SharedCode:podspec
در پروژه iOS میتوانید از این ارجاع به فایل podspec به صورت زیر استفاده کنید:
1"SharedCode",:path => 'path-to-shared-code/SharedCode.podspec'
سپس دستور زیر را اجرا کنید:
pod install
بدین ترتیب پیکربندیها قلاب میشوند و میتوانید از کد اشترکی در iOS نیز استفاده کنید. همچنین فریمورک و ارجاعی ایجاد میشود، اما همه کار از طریق cocoapods انجام مییابد. پس از انجام این کارها اینک میتوانیم آن را در یک ViewController ایمپورت کنیم.
1import SharedCode
2
3class ViewItemsViewController: UIViewController {
4 let viewModel = ViewItemsViewModel.init().create()
5
6 func viewDidAppear() {
7 viewModel.items.watch { items in
8 // Reload TableViewController
9 }
10 }
11}
بدین ترتیب موفق شدیم از کد اشتراکی در اندروید و iOS استفاده کنیم. جمعبندی بصری کارهایی که تا به این جا انجام دادیم به صورت زیر است:
سخن پایانی
در این مقاله از کاتلین چند پلتفرمی استفاده کردیم تا کد را بین اندروید و iOS به اشتراک بگذاریم. همچنین از کتابخانههای چند پلتفرمی مانند Ktor fvhd networking Serialization برای تجزیه JSON و از کوروتینها برای وظایف ناهمگام بهره گرفتیم. Kotlin Multiplatform بسیار نویدبخش است و با ارائه نسخه 1.4 کاتلین انتظار میرود شاهد عرضه موارد جدیدی از این فناوری باشیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- آموزش مقدماتی زبان برنامه نویسی کاتلین (Kotlin) برای توسعه اندروید (Android)
- زبان برنامه نویسی کاتلین (Kotlin) — راهنمای کاربردی
- برنامه نویسی اندروید با کاتلین — راهنمای شروع به کار
- آموزش برنامه نویسی iOS در ویندوز | راهنمای رایگان و جامع شروع به کار
==