من چندماهی میشه که مشغول ساخت پروژه های کراس پلتفرم با kmp بودم و زمانی که تصمیم گرفتم که همون build-logic یا build-src معروف که توی اندروید برای مدیریت وابستگی ها در اپ های ماژولار از اون استفاده میکردیم را برای kmp پیاده سازی کنم بعد از کنار هم قرار دادن چند تا سورس کد و این مقاله و ساعت ها تست و آزمایش تونستم کارو انجام بدم،دلایلی زیادی وجود داشت که به چالش خوردم مثلا مقاله بالا import ها را نیاورده بود و اندروید استادیو کد را شناسایی نمیکرد و عملا پیدا کردن پکیج هر کلاس خیلی سخت بود، علت دیگه اینکه مقاله بالا یکسری اشتباهات انجام داده بود و دیگه اینکه نسخه gradle ، کاتلین و خلاصه هیچ چیزی را عنوان نکرده بود که بشه مشکلات را برطرف کرد.
امیدوارم این مقاله به شما کمک کنه زودتر به نتیجه برسین.
اول پوشه build-logic را به پوشه root پروژه خودتون اضافه کنید هر چند مکان این پوشه دلخواهه
در پوشه build-logic یک فایل به نام settings.gradle.kts اضافه کنید و کدهای زیر در اون بنویسید:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
سپس فایل gradle.properties را هم با کد زیر به اون اضافه کنید:
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
سپس یک ماژول به نام convention ایجاد کنید و یک پکیج دلخواه به اون اضافه کنید تا اینجای کار باید پوشه build-logic به این صورت باشه:
خب حالا به فایل settings.gradle.kts در مسیر اصلی پروژه خودتون برین و به شکل زیر پوشه build-logic را به عنوان build script وارد کنید:
حالا به فایل build.gradle.kts در ماژول convention برین و کد زیر را درون اون بنویسید:
plugins {
`kotlin-dsl`
}
group = "com.example.upfiles.buildlogic" //your module name
dependencies {
compileOnly(libs.android.gradlePlugin) //if targetting Android
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.compose.gradlePlugin) //if you are using Compose Multiplatform
}
اگه در پروژه تون از version catalog استفاده نکردین لازمه ابتدا Version catalog را به پروژه تون اضافه کنید و گرنه در کد بالا به خطا برمیخورین
حالا لازمه مواردی که در کد بالا استفاده کردیم را در version catalog خودمون اضافه کنیم یعنی فایل libs.versions.toml در قسمت [libraries] :
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } compose-gradlePlugin = { module = "org.jetbrains.compose:org.jetbrains.compose.gradle.plugin", version.ref = "compose" }
در ابتدا باید به version catalog هامون دسترسی داشته باشیم تا راحت تر کد بنویسیم پس در ماژول convention یک فایل ایجاد کنید تا دسترسی به اون را فراهم کنیم:
package com.example.upfiles
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
val Project.libs
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
// if using android libraries
//val Project.androidLibs
// get(): VersionCatalog =extensions.getByType<VersionCatalogsExtension>().named("androidLibs")
حالا نیاز داریم به یک extension فانکشن تا سورس ست های اندروید را کانفیگ کنیم و از شر کد تکراری خلاص بشیم پس فایل جدیدی میسازیم و کد زیر را به اون اضافه میکنیم:
import com.android.build.api.dsl.LibraryExtension
import com.example.upfiles.libs
import org.gradle.api.Project
import org.gradle.api.JavaVersion
internal fun Project.configureKotlinAndroid(
extension: LibraryExtension
) = extension.apply {
// get module name from module path
val moduleName = path.split(":").drop(2).joinToString(".")
namespace = if(moduleName.isNotEmpty()) "com.example.upfiles.$moduleName" else "com.example.upfiles"
compileSdk = libs.findVersion("compileSdk").get().requiredVersion.toInt()
defaultConfig {
minSdk = libs.findVersion("minSdk").get().requiredVersion.toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
کد بالا تمام کانفیگ های سمت اندروید را انجام میده از min sdk و compile sdk گرفته تا نسخه جاوا و حتی namespace البته میتونین طبق سلیقتون تغییرش بدین یا قسمتیش را حذف کنید.
حالا همون کار بالا را برای Cocoapods هم تکرار میکنیم:
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
internal fun Project.configureKotlinCocoapods(
extension: CocoapodsExtension
) = extension.apply {
val moduleName = this@configureKotlinCocoapods.path
.split(":")
.drop(1)
.joinToString("-")
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
version = "1.0" //your cocoapods version
ios.deploymentTarget = "14.1" //your iOS deployment target
name = moduleName
framework {
isStatic = true //static or dynamic according to your project
baseName = moduleName
}
}
و در آخر نوبت به kotlin multiplatform میرسه مثل قبل فایل جدید ایجاد میکنیم و کدهای زیر را به اون اضافه میکنیم:
import com.example.upfiles.libs
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
internal fun Project.configureKotlinMultiplatform(
extension: KotlinMultiplatformExtension
) = extension.apply {
jvmToolchain(17)
// targets
androidTarget()
iosArm64()
iosX64()
iosSimulatorArm64()
//common dependencies
sourceSets.apply {
getByName("commonMain") {
dependencies {
implementation(libs.findLibrary("koin.core").get())
implementation(libs.findLibrary("coroutines.core").get())
implementation(libs.findLibrary("kotlinx-dateTime").get())
implementation(libs.findLibrary("napier").get())
}
}
getByName("androidMain") {
dependencies {
implementation(libs.findLibrary("koin.android").get() )
}
}
}
// applying the Cocoapods Configuration we made
(this as ExtensionAware).extensions.configure<CocoapodsExtension>(::configureKotlinCocoapods)
}
خب کار ما تموم شد فقط باید کدهایی که نوشتیم را تبدیل به کاستوم پلاگین کنیم تا بتونیم توی ماژول هامون ازشون استفاده کنیم.
یک کلاس به اسم KotlinMultiplatformPlugin میسازیم با کد زیر:
package com.example.upfiles
import com.android.build.api.dsl.LibraryExtension
import configureKotlinMultiplatform
import configureKotlinAndroid
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
class KotlinMultiplatformPlugin: Plugin<Project> {
override fun apply(target: Project):Unit = with(target){
with(pluginManager){
apply(libs.findPlugin("kotlinMultiplatform").get().get().pluginId)
apply(libs.findPlugin("kotlinCocoapods").get().get().pluginId)
apply(libs.findPlugin("androidLibrary").get().get().pluginId)
}
extensions.configure<KotlinMultiplatformExtension>(::configureKotlinMultiplatform)
extensions.configure<LibraryExtension>(::configureKotlinAndroid)
}
}
و یک کلاس دیگه به اسم ComposeMultiplatformPlugin :
package com.example.upfiles
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
class ComposeMultiplatformPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
with(pluginManager) {
apply(libs.findPlugin("composeMultiplatform").get().get().pluginId)
}
val composeDeps = extensions.getByType<ComposeExtension>().dependencies
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.apply {
getByName("commonMain") {
dependencies {
implementation(composeDeps.runtime)
implementation(composeDeps.foundation)
implementation(composeDeps.material3)
implementation(composeDeps.materialIconsExtended)
implementation(libs.findLibrary("androidx-activity-compose").get())
}
}
}
}
}
}
نوبت به رجیستر کردن پلاگین هامون رسید، کد فایل build.gradle.kts از ماژول convention را به صورت زیر آپدیت میکنیم:
plugins {
`kotlin-dsl`
}
group = "com.example.upfiles.buildlogic"
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.compose.gradlePlugin)
}
gradlePlugin {
plugins {
register("kotlinMultiplatform"){
id = "com.example.upfiles.kotlinMultiplatform"
implementationClass = "com.example.upfiles.KotlinMultiplatformPlugin"
}
register("composeMultiplatform"){
id = "com.example.upfiles.composeMultiplatform"
implementationClass = "com.example.upfiles.ComposeMultiplatformPlugin"
}
}
}
میتونیم برای زیباتر شدن کدهامون دو تا پلاگینی که رجیستر کردیم یعنی
com.example.upfiles.KotlinMultiplatformPlugin
com.example.upfiles.composeMultiplatform
را هم ببریم توی ورژن کاتالوگمون:
[plugins]
composeMultiplatform = { id = "com.example.upfiles.composeMultiplatform", version = "unspecified" }
kotlinMultiplatform = { id = "com.example.upfiles.kotlinMultiplatform", version = "unspecified" }
و تمام دیگه میتونیم فایل های Gradle ماژول هامون را خوانا تر بنویسیم
نمونه کد فایل ها قبل از تغییرات:
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.android.library")
id("org.jetbrains.compose")
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
kotlin {
targetHierarchy.default()
androidTarget()
ios()
iosArm64()
iosX64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(libs.koin.core)
implementation(libs.coroutines)
}
}
}
}
android {
compileSdk = libs.versions.compileSdk.get().toInt()
namespace = "com.example.upfiles.profile"
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/main/resources")
sourceSets["main"].resources.srcDirs("src/main/resources")
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
jvmToolchain(17)
}
}
نمونه کد ریفکتور شده:
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.kotlinMultiplatform)
kotlin("plugin.serialization") version libs.versions.kotlin.get()
}
همان طور که در برنامه نویسی اندروید ما به وسلیه gradle convention plugin کدهای بیلد ماژول های خود را خواناتر و منسجم تر میکنیم میتوانیم در Kotlin Multiplatform هم به آسانی همین کار را انجام دهیم.
متاسفانه با اینکه تلاش زیادی کردم موفق نشدم پلاگین هایی به عنوان نمونه kotlin serialization را به کاستوم پلاگین اضافه کنم و نتیجه جستجو در این مورد این بود که این کار ممکن نیست خوشحال میشم اگه راهی بلد هستین با بنده به اشتراک بزارید همچنین هر گونه بهبودی بنظرتون میرسه خوشحال میشم بگین
با تشکر