ساختن افزایه‌های Gradle سفارشی برای Android KMP

این سند راهنمایی برای نویسندگان افزونه ارائه می‌دهد درباره اینکه چگونه به‌درستی راه‌اندازی Kotlin Multiplatform (KMP) را شناسایی، پیکربندی، و با آن تعامل کنند، با تمرکز ویژه بر ادغام با اهداف Android در یک پروژه KMP. این توصیه‌ها چه درحال ساختن افزونه‌های قراردادی برای استانداردسازی پیکربندی‌ها در سراسر ماژول‌های پروژه‌تان باشید و چه درحال توسعه افزونه‌ها برای استفاده گسترده‌تر انجمن باشید، اعمال می‌شوند. As KMP continues to evolve, understanding the proper hooks and APIs—such as KotlinMultiplatformExtension, KotlinTarget types, and the Android-specific integration interfaces—is essential for building robust and future-proof tooling that works seamlessly across all platforms defined in a multiplatform project.

بررسی اینکه آیا پروژه از افزونه Kotlin Multiplatform استفاده می‌کند یا نه

برای جلوگیری از خطا و اطمینان از اینکه افزونه شما فقط زمانی اجرا می‌شود که KMP وجود دارد، باید بررسی کنید که آیا پروژه از افزونه KMP استفاده می‌کند یا نه. بهترین روش این است که از plugins.withId() برای واکنش به اعمال شدن افزایه KMP استفاده کنید، نه اینکه بلافاصله آن را بررسی کنید. این رویکرد واکنشی از شکننده بودن افزایه شما نسبت به ترتیبی که افزایه‌ها در اسکریپت‌های ساخت کاربر اعمال می‌شوند جلوگیری می‌کند.

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            // The KMP plugin is applied, you can now configure your KMP integration.
        }
    }
}

دسترسی به مدل

نقطه ورود برای همه پیکربندی‌های Kotlin چندپلاتفرمی، افزونه KotlinMultiplatformExtension است.

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
        }
    }
}

واکنش به هدف‌های Kotlin Multiplatform

از محفظه targets برای پیکربندی واکنشی افزایه برای هر هدفی که کاربر اضافه می‌کند استفاده کنید.

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                // 'target' is an instance of KotlinTarget
                val targetName = target.name // for example, "android", "iosX64", "jvm"
                val platformType = target.platformType // for example, androidJvm, jvm, native, js
            }
        }
    }
}

اعمال منطق هدف‌محور

اگر افزونه شما باید منطق را فقط برای انواع خاصی از پلاتفرم‌ها اعمال کند، یک رویکرد رایج بررسی ویژگی platformType است. این یک شمارشی است که به‌طور کلی هدف را دسته‌بندی می‌کند.

برای مثال، اگر افزونه شما فقط نیاز به تمایز کلی دارد (مثلاً فقط در هدف‌های شبیه JVM اجرا شود)، از این مورد استفاده کنید:

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                when (target.platformType) {
                    KotlinPlatformType.jvm -> { /* Standard JVM */ }
                    KotlinPlatformType.androidJvm -> { /* Android */ }
                    KotlinPlatformType.js -> { /* JavaScript */ }
                    KotlinPlatformType.native -> { /* Any Native (iOS, Linux, Windows, etc.) */ }
                    KotlinPlatformType.wasm -> { /* WebAssembly */ }
                    KotlinPlatformType.common -> { /* Metadata target (rarely needs direct plugin interaction) */ }
                }
            }
        }
    }
}

جزئیات مختص Android

While all Android targets have the platformType.androidJvm indicator, KMP has two distinct integration points depending on the Android Gradle plugin used: KotlinAndroidTarget for projects using com.android.library or com.android.application, and KotlinMultiplatformAndroidLibraryTarget for projects using com.android.kotlin.multiplatform.library.

‫KotlinMultiplatformAndroidLibraryTarget API در AGP 8.8.0 اضافه شده است، بنابراین اگر مصرف‌کنندگان افزایه شما از نسخه پایین‌تر AGP استفاده می‌کنند، بررسی target is KotlinMultiplatformAndroidLibraryTarget ممکن است منجر به ClassNotFoundException شود. برای ایمن کردن این مورد، AndroidPluginVersion.getCurrent() را قبل‌از بررسی نوع هدف بررسی کنید. توجه داشته باشید که AndroidPluginVersion.getCurrent() به AGP نسخه ۸.۳.۱ یا بالاتر نیاز دارد.

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                if (target is KotlinAndroidTarget) {
                    // Old kmp android integration using com.android.library or com.android.application
                } else if (
                    // Check platformType first, so AGP classes load only for Android targets
                    target.platformType == KotlinPlatformType.androidJvm &&
                    AndroidPluginVersion.getCurrent() >= AndroidPluginVersion(8, 8) &&
                    target is KotlinMultiplatformAndroidLibraryTarget
                ) {
                    // New kmp android integration using com.android.kotlin.multiplatform.library
                }
            }
        }
    }
}

دسترسی به افزونه KMP Android و ویژگی‌های آن

افزونه شما عمدتاً با افزونه Kotlin ارائه‌شده توسط افزونه Kotlin Multiplatform و افزونه Android ارائه‌شده توسط AGP برای هدف KMP Android تعامل خواهد داشت. بلوک android {} در افزونه Kotlin در پروژه KMP با میانای KotlinMultiplatformAndroidLibraryTarget نشان داده می‌شود که KotlinMultiplatformAndroidLibraryExtension را نیز گسترش می‌دهد. این یعنی می‌توانید ازطریق این شیء واحد به ویژگی‌های DSL مختص هدف و مختص Android دسترسی داشته باشید.

import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)

            // Access the Android target, which also serves as the Android-specific DSL extension
            kmpExtension.targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java).configureEach { androidTarget ->

                // You can now access properties and methods from both
                // KotlinMultiplatformAndroidLibraryTarget and KotlinMultiplatformAndroidLibraryExtension
                androidTarget.compileSdk = 34
                androidTarget.namespace = "com.example.myplugin.library"
                androidTarget.withJava() // enable Java sources
            }
        }
    }
}

برخلاف دیگر افزایه‌های Android (مثل com.android.library یا com.android.application)، افزایه KMP Android افزونه DSL اصلی خود را در سطح پروژه ثبت نمی‌کند. این در سلسله‌مراتب هدف KMP قرار دارد تا اطمینان حاصل شود که فقط برای هدف خاص Android تعریف‌شده در راه‌اندازی چندپلتفرمی شما اعمال می‌شود.

مدیریت مجموعه‌های منبع و کامپایل‌ها

اغلب، افزونه‌ها باید در سطحی دقیق‌تر از صرفاً هدف کار کنند—به‌طور مشخص، آن‌ها باید در سطح گردآوری کار کنند. ‫KotlinMultiplatformAndroidLibraryTarget حاوی KotlinMultiplatformAndroidCompilation نمونه است (برای مثال، main، hostTest، deviceTest). هر کامپایل با مجموعه‌های منبع Kotlin مرتبط است. افزایه‌ها می‌توانند با این‌ها تعامل داشته باشند تا منابع و وابستگی‌ها را اضافه کنند یا تکالیف گردآوری را پیکربندی کنند.

import com.android.build.api.dsl.KotlinMultiplatformAndroidCompilation
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                target.compilations.configureEach { compilation ->
                    // standard compilations are usually 'main' and 'test'
                    // android target has 'main', 'hostTest', 'deviceTest'
                    val compilationName = compilation.name

                    // Access the default source set for this compilation
                    val defaultSourceSet = compilation.defaultSourceSet

                    // Access the Android-specific compilation DSL
                    if (compilation is KotlinMultiplatformAndroidCompilation) {

                    }

                    // Access and configure the Kotlin compilation task
                    compilation.compileTaskProvider.configure { compileTask ->

                    }
                }
            }
        }
    }
}

پیکربندی کردن کامپایل‌های آزمایشی در افزایه‌های قراردادی

هنگام پیکربندی مقادیر پیش‌فرض برای کامپایل‌های آزمایشی (مانند targetSdk برای آزمایش‌های ابزاری) در افزایه قراردادی، باید از روش‌های فعال‌سازی مانند withDeviceTest { } یا withHostTest { } استفاده نکنید. فراخوانی این روش‌ها مشتاقانه باعث ایجاد انواع تست Android مربوطه و کامپایل برای هر ماژولی می‌شود که افزونه قرارداد را اعمال می‌کند، که ممکن است مناسب نباشد. علاوه‌براین، این روش‌ها را نمی‌توان برای بار دوم در یک ماژول خاص برای پالایش تنظیمات فراخوانی کرد، زیرا انجام این کار خطایی را ایجاد می‌کند که نشان می‌دهد کامپایل قبلاً ایجاد شده است.

درعوض، توصیه می‌کنیم از بلوک configureEach واکنشی در محفظه مجموعه‌ها استفاده کنید. این به شما امکان می‌دهد پیکربندی‌های پیش‌فرضی ارائه دهید که فقط درصورتی اعمال می‌شوند که یک ماژول به‌طور صریح کامپایل آزمایشی را فعال کند:

import com.android.build.api.dsl.KotlinMultiplatformAndroidDeviceTestCompilation
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmpExtension =
                project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
                .configureEach { androidTarget ->
                    androidTarget.compilations.withType(
                        KotlinMultiplatformAndroidDeviceTestCompilation::class.java
                    ).configureEach {
                        it.targetSdk { version = release(34) }
                    }
                }
        }
    }
}

این الگو تضمین می‌کند که افزونه قراردادی شما تنبل باقی می‌ماند و به ماژول‌های جداگانه اجازه می‌دهد تا withDeviceTest { } را برای فعال کردن و سفارشی‌سازی بیشتر آزمایش‌هایشان بدون تداخل با پیش‌فرض‌ها فراخوانی کنند.

تعامل با Variant API

برای تکالیفی که به پیکربندی مرحله آخر، دسترسی به آرتیفکت (مانند مانیفست‌ها یا بایت‌کد)، یا توانایی فعال یا غیرفعال کردن مؤلفه‌های خاص نیاز دارند، باید از Android Variant API استفاده کنید. در پروژه‌های KMP، افزونه از نوع KotlinMultiplatformAndroidComponentsExtension است.

وقتی افزونه KMP Android اعمال می‌شود، این افزونه در سطح پروژه ثبت می‌شود.

از beforeVariants برای کنترل ایجاد متغیرها یا مؤلفه‌های آزمایشی تودرتوی آن‌ها (hostTests و deviceTests) استفاده کنید. این مکان مناسبی برای غیرفعال کردن برنامه‌نویسی‌شده آزمایش‌ها یا تغییر مقادیر ویژگی‌های DSL است.

import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val androidComponents = project.extensions.findByType(KotlinMultiplatformAndroidComponentsExtension::class.java)
            androidComponents?.beforeVariants { variantBuilder ->
                // Disable all tests for this module
                variantBuilder.hostTests.values.forEach { it.enable = false }
                variantBuilder.deviceTests.values.forEach { it.enable = false }
            }
        }
    }
}

از onVariants برای دسترسی به شیء متغیر نهایی (KotlinMultiplatformAndroidVariant) استفاده کنید. در اینجا می‌توانید ویژگی‌های حل‌شده را بررسی کنید یا تبدیل‌ها را در مصنوعاتی مانند مانیفست ادغام‌شده یا کلاس‌های کتابخانه ثبت کنید.

import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val androidComponents = project.extensions.findByType(KotlinMultiplatformAndroidComponentsExtension::class.java)
            androidComponents?.onVariants { variant ->
                // 'variant' is a KotlinMultiplatformAndroidVariant
                val variantName = variant.name

                // Access the artifacts API
                val manifest = variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}