آموزش Kotlin Multiplatform(قسمت دوم)

مقدمه و قسمت اول رو میتونین تو کانال Android Corner ببینید.ساخت BuildSrcیکی از ویژگی های خوب نسخه های جدید Gradle، مفهوم BuildSrc هستش. یه ماژوله که می تونید توش ورژن های variable و پلاگینا تون رو تعریف کنید. برای ساختن این ماژول، باید توی پوشه روت پروژه و New ▸ Directory رو بزنید و اسم پوشه رو هم بزارید buildSrc. از اونجایی که این یه ماژوله، نیاز به یه فایل build داره --> پس توش یه فایل بنام build.gradle.kts (دقت کنید پسوندش.kts باشه) و کد زیر رو توش اضافه کنید.repositories { mavenCentral() } plugins { kotlin - dsl }خب حالا gradle رو sync کنید. بعد دوباره کلیک-راست کنید روی buildSrc و src/main/kotlin رو انتخاب کنید. حالا توی پوشه kotlin، یه فایل بنام Dependencies بسازید. تو این فایل variable هایی رو تعریف میکنیم که plugin ها و dependency هامون رو مشخص میکنن. این فایل رو ایجاد میکنیم تا همه ورژن های dependency و plugin ها یکجا باشن و مدیریت شون راحت بشه.همه پلاگینا رو با این کد تعریف کنید:const val androidPlugin = &quotandroid&quot const val androidApp = &quotcom.android.application&quot const val androidLib = &quotcom.android.library&quot const val multiplatform = &quotmultiplatform&quot const val composePlugin = &quotorg.jetbrains.compose&quot const val cocopods = &quotnative.cocoapods&quotقدم بعد اضافه کردن ورژن هاست:object Versions { // 1 const val min_sdk = 24 const val target_sdk = 31 const val compile_sdk = 31 // 2 // Plugins const val kotlin = &quot1.6.10&quot const val kotlin_gradle_plugin = &quot1.6.10&quot const val android_gradle_plugin = &quot7.0.4&quot const val desktop_compose_plugin = &quot1.0.1&quot const val compose_compiler_version = &quot1.1.0-rc02&quot const val compose_version = &quot1.1.0-rc01&quot // TODO: Add Other versions } // TODO: Add Deps1- اینا ورژن های sdk ی اندروید رو مشخص میکنن و برای تعداد لایبرری هم حداقل(min) SDK باید 24 باشه.2- ورژن های پلاگین تونه.یه سری ورژن های لایبرری های دیگه هم مونده اونا رو اضافه می کنیم. بجای// TODO: Add Other versionsبنویسید:const val coroutines = &quot1.5.0-native-mt&quot const val junit = &quot4.13.2&quot const val material = &quot1.4.0&quot const val kotlinxDateTime = &quot0.3.1&quot const val activity_compose = &quot1.4.0&quot const val napier = &quot2.1.0&quot const val junit5 = &quot1.5.10&quot const val frameworkName = &quotshared&quotو بجای// TODO: Add Depsاینو قرار بدید:object Deps { const val android_gradle_plugin = &quotcom.android.tools.build:gradle:${Versions.android_gradle_plugin}&quot const val kotlin_gradle_plugin = &quotorg.jetbrains.kotlin:kotlin- gradle - plugin: $ { Versions.kotlin_gradle_plugin} &quot const val junit = &quotjunit:junit:${Versions.junit}&quot const val material = &quotcom.google.android.material:material:${Versions.material}&quot const val napier = &quotio.github.aakira:napier:${Versions.napier}&quot // TODO: Add Compose }این variable ها برای پلاگین ها و چندتا لایبرری هستش.قدم بعد اضافه کردن کتابخونه های compose. بجای// TODO: Add Composeاینو بنویسید:object Compose { const val ui = &quotandroidx.compose.ui:ui:${Versions.compose_version}&quot const val uiUtil = &quotandroidx.compose.ui:ui- util: $ { Versions.compose_version } &quot const val tooling = &quotandroidx.compose.ui:ui- tooling: $ { Versions.compose_version } &quot const val foundation = &quotandroidx.compose.foundation:foundation:${Versions.compose_version}&quot const val material = &quotandroidx.compose.material:material:${Versions.compose_version}&quot const val material_icons = &quotandroidx.compose.material:material- icons - extended: $ { Versions.compose_version }&quot const val runtime = &quotandroidx.compose.runtime:runtime:${Versions.compose_version}&quot const val compiler = &quotandroidx.compose.compiler:compiler:${Versions.compose_version}&quot const val runtime_livedata = &quotandroidx.compose.runtime:runtime- livedata: $ { Versions.compose_version } &quot const val foundationLayout = &quotandroidx.compose.foundation:foundation- layout: $ { Versions.compose_version } &quot const val activity = &quotandroidx.activity:activity-compose: $ { Versions.activity_compose } &quot } // TODO: Add Coroutinesحالا یه سری کتابخونه برای Coroutine رو اضافه میکنیم، بجای :// TODO: Add Coroutinesobject Coroutines { const val common = &quotorg.jetbrains.kotlinx:kotlinx-coroutines- core: $ {Versions.coroutines }&quot const val android = &quotorg.jetbrains.kotlinx:kotlinx-coroutines- android: $ { Versions.coroutines} &quot const val test = &quotorg.jetbrains.kotlinx:kotlinx-coroutines- test: $ {Versions.coroutines } &quot } // TODO: Add JetBrainsحالا باید تعدادی کتابخونه jetbrains برای dataTime و desktop compose رو بجای// TODO: Add JetBrainsاضافه کنیم:object JetBrains { const val datetime = &quotorg.jetbrains.kotlinx:kotlinx- datetime: $ { Versions.kotlinxDateTim }&quot const val uiDesktop = &quotorg.jetbrains.compose.ui:ui- desktop: $ { Versions.desktop_compose_plugin } &quot const val uiUtil = &quotorg.jetbrains.compose.ui:ui- util: $ { Versions.desktop_compose_plugin} &quot }حالا میتونید پروژه sync کنید.فایل build ماژول sharedفایل build.gradle.kts رو باز کنید و اینو جایگزین پلاگین ها بکنید:kotlin(multiplatform) id(androidLib) kotlin(cocopods)الان gradle بصورت خودکار(اتوماتیک) این ثابت(constant) هایی که تعریف کردیم تو buildSrc رو میخونه.تو بخش اندروید هم اینو جایگزین کنید:android { compileSdk = Versions.compile_sdk sourceSets[&quotmain&quot].manifest.srcFile(&quotsrc/androidMain/AndroidManifes t.xml &quot) defaultConfig { minSdk = Versions.min_sdk targetSdk = Versions.target_sdk } }ورژن ها رو جایگزین کردیم با اونچه که تو buildSrc تعریف کرده بودیم.فایل build.gradle.kts رو توی root باز کنید و اینو جایگزین string های classpath کنید.classpath(Deps.android_gradle_plugin) classpath(Deps.kotlin_gradle_plugin)حالا gradle رو sync کنید تا مطمئن شیم که کار میکنه.فایل build برای ماژول اندرویدفایل androidApp -> build.gradle.kts رو باز کنید و جایگزین کنید اینو بجای بخش plugin:plugins { id(androidApp) kotlin(androidPlugin) }بخش dependencies رو با این جایگیزین کنید:dependencies { // 1 implementation(project(&quot:shared&quot)) // 2 with(Deps) { implementation(napier) implementation(material) } // 3 //Compose with(Deps.Compose) { implementation(compiler) implementation(runtime) implementation(runtime_livedata) implementation(ui) implementation(tooling) implementation(foundation) implementation(foundationLayout) implementation(material) implementation(material_icons) implementation(activity) } }1- این، ماژول shared رو import میکنه.2- این napier رو تعریف میکنه(یه لایبرری multiplatform واسه logging) و کتابخونه material. برای جلوگیری از تکرار کد و خوانایی از with استفاده کردیم(بزودی یه مقاله در مورد scope فانکشن ها تو کانال منتشر میکنم)3- اینا هم که کتابخونه های compose هستش.حالا بخش android رو با این عوض کنید:android { compileSdk = Versions.compile_sdk defaultConfig { applicationId = &quotcom.raywenderlich.findtime.android&quot minSdk = Versions.min_sdk targetSdk = Versions.target_sdk versionCode = 1 versionName = &quot1.0&quot } buildTypes { getByName(&quotrelease&quot) { isMinifyEnabled = false } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = &quot1.8&quot } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = Versions.compose_compiler_version } }یه بار دیگه پروژه رو sync کنید و اپ رو اجرا کنید تا ببینید همه چیز درسته.شروع ساخت اپ Find Timeاین اپ چیکار میکنه: مثلا فرض کنید چندتا همکار تو ساعت های زمانی متفاوتی کار میکنن، حالا اگه بخوان یه جلسه برگزار کنن، باید بهترین تایم رو پیدا کنن تا با هم هماهنگ بشن. این اپ بهترین زمان ممکن رو پیدا میکنه.ممکنه که این پروژه بنظر ساده بیاد ولی .. کُلللیی چیز بهتون یاد میده که تو پروژه های multiplatform تون میتونین بعدا استفاده کنید.ساخت منطق بیزینس(business logic)یه مزیت که KMP داره اینه میتونیم business logic مون رو یه بار بنویسیم تو ماژول shared و بین همه پلتفرم به اشتراک بزاریمش.برین توی ماژول shared توی androidMain ،commonMain و iosMain، کلاس Platform و Greeting رو پاک کنید و تو ماژول Android و iOS که از این کلاسا استفاده شده هم برین و پاک شون کنید.محاسبات Datetimeما از کتابخونه kotlinx-datetime استفاده میکنیم برای محاسبات bash. فایل shared/build.gradle.kts رو باز کنید، val commonMain رو پیدا کنید و این dependency ها رو اضافه کنید؛ اینجوری:val commonMain by getting { dependencies { // 1 implementation(Deps.JetBrains.datetime) // 2 implementation(Deps.napier) } }و گریدل رو sync کنین.کتابخونه Datetimeلایبرری Kotlinx-DateTime یه کتابخونه مالتی پلتفرمه که کار کردن باهاش ساده است و کمک میکنه محاسبات براساس تاریخ/زمان رو انجام بدیم باهاش. از data type های مختلفی هم استفاده میکنه که:کلاس Instant به ما یه لحظه رو تو زمان نشون میده.کلاس Clock مثل یه ساعت هستش که لحظه فعلی رو به ما میگه.کلاس LocalDateTime به ما یه تاریخ و زمان نشون میده بدون زمان اون منطقه(اگه متوجه نشدید، بعدا تو کد متوجه میشید!!)کلاس LocalDate به ما فقط یه تاریخ میده.کلاس TimeZone و ZoneOffset برای تبدیل کردن Instant و LocalDateTime به کار میره.کلاس DateTimePeriod فاصله زمانی بین دو لحظه رو بهمون میده.کلاس DatePeriod یه زیر کلاسِ DateTimePeriod هستش و تفاوت زمانی دو LocalDate رو بهمون میده.اینام کلاس(enum) Month نمایانگر ماه های ساله.اینام کلاس(enum) DayOfWeek نمایانگر روز های هفته.و اینام کلاس DateTimeUnit یه سری واحد متداول برای محاسبات بهمون میده.کمک کننده منطقه زمانی(Time Zone Helper)قبل دست به کد شدن، برید اینجاا تمام سورس کد های کتاب داخل این ریپازیتوری هستش. ولی برای الان پروژه دوم یعنی02-getting-startedرو سعی کنید build کنید یا از پکیج name مشابه استفاده کنید چونکه این کتاب موقع توضیح دادن از پکیج name این پروژه استفاده کرده و شاید کمی گیج بشید.برید تو پکیجshared/commonMain/و یه interface بنام TimeZoneHelper بسازید:interface TimeZoneHelper { fun getTimeZoneStrings(): List < String > fun currentTime(): String fun currentTimeZone(): String fun hoursFromTimeZone(otherTimeZoneId: String): Double fun getTime(timezoneId: String): String fun getDate(timezoneId: String): String fun search(startHour: Int, endHour: Int, timezoneStrings: List < String > ): List < Int > }هفت function به ترتیب:لیستی از رشته های منطقه زمانی رو برمی گردونه.زمان فرمت شده فعلی رو بهمون میده.شناسه(id) منطقه فعلی رو برمیگردونه.ساعت ها رو از منطقه زمانی معین رپرتاژ برامون برمیگردونه.زمان فرمت شده رو برای یه منطقه زمانی مشخص برمیگردونه.تاریخ فرمت شده رو با توجه به اون منطقه زمانی مشخص برمیگردونه.لیست ساعت هایی که از Starthour شروع می شه و در Endhour به پایان میرسه رو جستجو میکنه.خب حالا باید یه کلاسی بنام TimeZoneHelperImpl رو بسازیم تا این interface رو implement کنه:class TimeZoneHelperImpl: TimeZoneHelper { }متد هارو Implement کنید.خب بیاین با getTimeZoneStrings شروع کنیم. تو بدنه این متد، اینو قرار بدید:return TimeZone.availableZoneIds.sorted()این خط شناسه های(id) منطقه زمانی موجود رو برمی گردونه و اونها رو مرتب می کنه. اگه TimeZone قرمز شد، cursor موس تون رو روش قرار بدید و کلیک راست کنید و لایبرری رو import کنید.قدم بعد باید یه متد بنویسیم برای قالب بندی(فرمت) datetime کلاس LocalDateTime؛ این متد رو پایین کلاس(TimeZoneHelperImpl) قرار بدین:fun formatDateTime(dateTime: LocalDateTime): String { // 1 val stringBuilder = StringBuilder() // 2 var hour = dateTime.hour val minute = dateTime.minute var amPm = &quot am&quot // 3 // For 12 if (hour > 12) { amPm = &quot pm&quot hour -= 12 } // 4 stringBuilder.append(hour.toString()) stringBuilder.append(&quot:&quot) // 5 if (minute < 10) { stringBuilder.append('0') } stringBuilder.append(minute.toString()) stringBuilder.append(amPm) // 6 return stringBuilder.toString() }1- از StringBuilder برای ساخت، تغییر و کلا مدیریت string ها استفاده میکنیم.2- فقط دوتا property رو از آرگمان این fun گرفتیم.3- اینم چک میکنه اگه ساعت(hour) از 12 بیشتر بود بیا pm نشون بده.4- رشته hour رو میسازه. خط بعدی هم اینو " : " اضافه میکنه به جلوی رشته hour.5- چک میکنه اگه دقیقه کمتر از 10 بود بیا یه صفر بزار اولش. و دقیقه رو میسازه و pm/am رو هم اخرش اضافه میکنه.6- رشته نهایی رو برمیگردونه.حالا بریم سراغ متد currentTime:override fun currentTime(): String { // 1 val currentMoment: Instant = Clock.System.now() // 2 val dateTime: LocalDateTime = currentMoment.toLocalDateTime(TimeZone.currentSystemDefault()) // 3 return formatDateTime(dateTime) }1- زمان فعلی رو به صورت لحظه دریافت میکنه.2- لحظه فعلی رو بر اساس منطقه زمانی کاربر به یک LocalDateTime تبدیل میکنه.3- و در آخر فرمتش میکنیم با متد formatDateTime که قبلا پیاده سازی کردیم.حالا نوبت پیاده سازی متد getTime:override fun getTime(timezoneId: String): String { // 1 val timezone = TimeZone.of(timezoneId) // 2 val currentMoment: Instant = Clock.System.now() // 3 val dateTime: LocalDateTime = currentMoment.toLocalDateTime(timezone) // 4 return formatDateTime(dateTime) }1- منطقه زمانی رو با استفاده از id که بهش دادیم بهمون میده.2- زمان فعلی رو به صورت لحظه بهمون میده.3- لحظه فعلی رو بر اساس منطقه زمانی کاربر به یک LocalDateTime تبدیل میکنه.4- فرمت میکنه!متد getDate هم شبیه getTime. برای getDate اینو بنویسید:override fun getDate(timezoneId: String): String { val timezone = TimeZone.of(timezoneId) val currentMoment: Instant = Clock.System.now() val dateTime: LocalDateTime = currentMoment.toLocalDateTime(timezone) // 1 return &quot${dateTime.dayOfWeek.name.lowercase().replaceFirstChar { it.uppercase() } }, &quot + &quot${dateTime.month.name.lowercase().replaceFirstChar { it.uppercase() } } $ { dateTime.date.dayOfMonth } &quot }1- این بخش های مختلف DateTime رو میگیره برای ساخت همچین رشته ای :Monday, October 4متد currentTimeZone هم که ساده ست. منطقه زمانی فعلی رو به عنوان یه رشته برمیگردونه:override fun currentTimeZone(): String { val currentTimeZone = TimeZone.currentSystemDefault() return currentTimeZone.toString() }برای متد hoursFromTimeZone هم باید ساعت هارو با توجه منطقه زمانی برگردونیم:override fun hoursFromTimeZone(otherTimeZoneId: String): Double { // 1 val currentTimeZone = TimeZone.currentSystemDefault() // 2 val currentUTCInstant: Instant = Clock.System.now() // Date time in other timezone // 3 val otherTimeZone = TimeZone.of(otherTimeZoneId) // 4 val currentDateTime: LocalDateTime = currentUTCInstant.toLocalDateTime(currentTimeZone) // 5 val currentOtherDateTime: LocalDateTime = currentUTCInstant.toLocalDateTime(otherTimeZone) // 6 return abs((currentDateTime.hour - currentOtherDateTime.hour) * 1.0) }1- منطقه زمانی فعلی رو دریافت میکنیم2- زمان/لحظه فعلی رو دریافت میکنیم.3-یه منطقه زمانی دیگه میسازیم.4- زمان فعلی رو به یک کلاس LocalDateTime تبدیل میکنیم.5- زمان فعلی رو تو منطقه زمانی(otherTimeZone) به یک کلاس LocalDateTime تبدیل میکنیم.6- اختلاف مطلق بین ساعت ها رو برمیگردونیم (نباید منفی باشه).جستجوجستجو کمی سخت تره. با توجه به یه ساعت شروع (مثلا 8 صبح,am)و یه ساعت پایان (مثلا 5 بعد از ظهر,pm) و لیست مناطق زمانی افراد(منظور اوناییه که میخوان هماهنگ بشن)، ما می خوایم یه لیست int که نمایانگر ساعت های (0 تا 23) هست رو دربیاریم که برای همه مناسب باشه. برای مثال: توی لس آنجلس 8صبح am تا 5 بعدازظهر pm میشه این:[8, 9, 10, 11, 12, 13, 14]ولی تو همون رنج (range) ساعتی 8 تا 5 تو نیویورک میشه این:[11, 12, 13, 14, 15, 16, 17]حالا باید یه سری time ها رو بکشیم بیرون از این دوتا تا یه زمان مناسب برای هردو پیدا، مثلا 11،12و.. .خب برای اینکه بفهمیم اون ساعت ای که بین این دو منطقه پیدا کردیم درسته، باید یه متد بنام isValid اضافه کنیم(بعد از متد search قرارش بدین):private fun isValid( timeRange: IntRange, hour: Int, currentTimeZone: TimeZone, otherTimeZone: TimeZone ): Boolean { if (hour!in timeRange) { return false } // TODO: Add Current Time }خب حالا این کد ها رو جاگیزین// TODO: Add Current Timeکنید:// 1 val currentUTCInstant: Instant = Clock.System.now() // 2 val currentOtherDateTime: LocalDateTime = currentUTCInstant.toLocalDateTime(otherTimeZone) // 3 val otherDateTimeWithHour = LocalDateTime( currentOtherDateTime.year, currentOtherDateTime.monthNumber, currentOtherDateTime.dayOfMonth, hour, 0, 0, 0 ) // TODO: Add Conversions1- این برای دریافت لحظه فعلی هستش2- با داشتن یه منطقه زمانی، لحظه رو به یه منطقه زمانی دیگر تبدیل میکنه.3- دریافت کردن LocalDateTime با یک ساعت مشخص. (دقیقه، ثانیه و نانو ثانیه لازم نیست)حالا بجای// TODO: Add Conversionsاین رو قرار بدید:// 1 val localInstant = otherDateTimeWithHour.toInstant(currentTimeZone) // 2 val convertedTime = localInstant.toLocalDateTime(otherTimeZone) Napier.d(&quotHour $hour in Time Range ${otherTimeZone.id} is $ { convertedTime.hour } &quot) // 3 return convertedTime.hour in timeRangeلایبرری Napier رو واسه گزارش کردن لازم داریم پس لایبرری اش رو import کنید اگه قرمزه.1- ساعته رو به منطقه زمانی فعلی تبدیل میکنه.2- ساعت منطقه زمانی رو به منطقه زمانی دیگه تبدیل میکنه.3- بررسی میکنه که آیا در محدوده زمانی شما است یا خیر.حالا بجای// TODO: Add Conversionsاین رو قرار بدید:// 1 val localInstant = otherDateTimeWithHour.toInstant(currentTimeZone) // 2 val convertedTime = localInstant.toLocalDateTime(otherTimeZone) Napier.d(&quotHour $hour in Time Range ${otherTimeZone.id} is $ { convertedTime.hour } &quot) // 3 return convertedTime.hour in timeRangeلایبرری Napier رو واسه گزارش کردن لازم داریم پس لایبرری هاش رو import کنید اگه قرمزه.1- ساعته رو به منطقه زمانی فعلی تبدیل میکنه.2- ساعت منطقه زمانی رو به منطقه زمانی دیگه تبدیل میکنه.3- بررسی میکنه که آیا در محدوده زمانی شما است یا خیر.خب حالا شما متد isValid رو دارید. پس بیاین متد search رو پیاده سازی کنیم(اندازه isValid سخت نیست!!). فقط باید مناطق و ساعتهای معین رو بررسی کنیم که آیا معتبر هستن یا نه. اینجوری:// 1 val goodHours = mutableListOf < Int > () // 2 val timeRange = IntRange(max(0, startHour), min(23, endHour)) // 3 val currentTimeZone = TimeZone.currentSystemDefault() // 4 for (hour in timeRange) { var isGoodHour = false // 5 for (zone in timezoneStrings) { val timezone = TimeZone.of(zone) // 6 if (timezone == currentTimeZone) { continue } // 7 if (!isValid( timeRange = timeRange, hour = hour, currentTimeZone = currentTimeZone, otherTimeZone = timezone )) { Napier.d(&quotHour $hour is not valid for time range&quot) isGoodHour = false break } else { Napier.d(&quotHour $hour is Valid for time range&quot) isGoodHour = true } } // 8 if (isGoodHour) { goodHours.add(hour) } } // 9 return goodHours1- لیست تایم هایی که valid هستن.2- یه محدوده range میسازه.3- زمان فعلی منطقه رو بهمون میده.4- سرتاسر محدوده ساعت رو میگرده5- سراسر محدوده منطقه زمانی رو میگرده تو لیست منطقه زمانی.6- اگه منطقه زمانی مشترک باشه یعنی خوبه.7- بعد از اینکه سراسر محدوده(range) رو گشت و time(ساعت) مناسب رو پیدا کرد، اونو به لیست اضافی میکنه.8-لیست ساعت های مناسب رو return میکنه.خبب! تبریک میگم، شما business logic این اپ رو نوشتین، اگه جایی براتون گنگ بود، برید دوباره اون بخش رو بخونید و یا تو کانال زیر این پست سوالتون رو به اشتراک بزارید تا حلش کنیم.امیدوارم خوشتون بیاد. اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین.کانال Android Corner