محمد جواد صفاتاج
محمد جواد صفاتاج
خواندن ۷ دقیقه·۶ ماه پیش

استفاده از gradle convention plugin در KMP

1-مقدمه

من چندماهی میشه که مشغول ساخت پروژه های کراس پلتفرم با kmp بودم و زمانی که تصمیم گرفتم که همون build-logic یا build-src معروف که توی اندروید برای مدیریت وابستگی ها در اپ های ماژولار از اون استفاده میکردیم را برای kmp پیاده سازی کنم بعد از کنار هم قرار دادن چند تا سورس کد و این مقاله و ساعت ها تست و آزمایش تونستم کارو انجام بدم،دلایلی زیادی وجود داشت که به چالش خوردم مثلا مقاله بالا import ها را نیاورده بود و اندروید استادیو کد را شناسایی نمیکرد و عملا پیدا کردن پکیج هر کلاس خیلی سخت بود، علت دیگه اینکه مقاله بالا یکسری اشتباهات انجام داده بود و دیگه اینکه نسخه gradle ، کاتلین و خلاصه هیچ چیزی را عنوان نکرده بود که بشه مشکلات را برطرف کرد.

امیدوارم این مقاله به شما کمک کنه زودتر به نتیجه برسین.

2-آماده کردن پروژه

اول پوشه 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 = &quotcom.android.tools.build:gradle&quot, version.ref = &quotagp&quot } kotlin-gradlePlugin = { module = &quotorg.jetbrains.kotlin:kotlin-gradle-plugin&quot, version.ref = &quotkotlin&quot } compose-gradlePlugin = { module = &quotorg.jetbrains.compose:org.jetbrains.compose.gradle.plugin&quot, version.ref = &quotcompose&quot }

3-شروع به کد نویسی

در ابتدا باید به 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()
}

4-سخن پایانی

همان طور که در برنامه نویسی اندروید ما به وسلیه gradle convention plugin کدهای بیلد ماژول های خود را خواناتر و منسجم تر میکنیم میتوانیم در Kotlin Multiplatform هم به آسانی همین کار را انجام دهیم.

متاسفانه با اینکه تلاش زیادی کردم موفق نشدم پلاگین هایی به عنوان نمونه kotlin serialization را به کاستوم پلاگین اضافه کنم و نتیجه جستجو در این مورد این بود که این کار ممکن نیست خوشحال میشم اگه راهی بلد هستین با بنده به اشتراک بزارید همچنین هر گونه بهبودی بنظرتون میرسه خوشحال میشم بگین

با تشکر


kotlinandroidgradle
من برنامه نویس اندروید و kmp هستم با 2 الی 3 سال تحقیق و تجربه و دوست دارم به یک برنامه نویس حرفه‌ای تبدیل بشم
شاید از این پست‌ها خوشتان بیاید