Tạo trình bổ trợ Gradle tuỳ chỉnh cho KMP Android

Hướng dẫn này giải thích cách phát hiện, tương tác và định cấu hình mục tiêu Android trong dự án Kotlin Multiplatform (KMP) từ một trình bổ trợ Gradle tuỳ chỉnh. Hãy sử dụng các API này khi viết trình bổ trợ quy ước cho bản dựng hoặc xuất bản trình bổ trợ Gradle cho các dự án KMP.

Kiểm tra xem dự án có sử dụng trình bổ trợ Kotlin Multiplatform hay không

Để tránh lỗi và đảm bảo rằng trình bổ trợ của bạn chỉ chạy khi có KMP, bạn phải kiểm tra xem dự án có sử dụng trình bổ trợ KMP hay không. Tốt nhất là bạn nên sử dụng plugins.withId() để phản ứng với trình bổ trợ KMP đang được áp dụng, thay vì kiểm tra trình bổ trợ đó ngay lập tức. Phương pháp phản ứng này giúp trình bổ trợ của bạn không bị ảnh hưởng bởi thứ tự áp dụng trình bổ trợ trong tập lệnh bản dựng của người dùng.

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.
        }
    }
}

Sử dụng mô hình

Điểm truy cập cho tất cả cấu hình Kotlin Multiplatform là tiện ích 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)
        }
    }
}

Phản ứng với các mục tiêu Kotlin Multiplatform

Sử dụng vùng chứa targets để định cấu hình trình bổ trợ một cách linh hoạt cho từng mục tiêu mà người dùng thêm vào.

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
            }
        }
    }
}

Áp dụng logic theo mục tiêu cụ thể

Nếu trình bổ trợ của bạn chỉ cần áp dụng logic cho một số loại nền tảng nhất định, thì cách phổ biến là kiểm tra thuộc tính platformType. Đây là một enum phân loại rộng rãi mục tiêu.

Ví dụ: hãy sử dụng thuộc tính này nếu trình bổ trợ của bạn chỉ cần phân biệt một cách khái quát (chẳng hạn như chỉ chạy trên các mục tiêu giống 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) */ }
                }
            }
        }
    }
}

Thông tin chi tiết dành riêng cho Android

Mặc dù tất cả mục tiêu Android đều có chỉ báo platformType.androidJvm, nhưng KMP có hai điểm tích hợp riêng biệt tuỳ thuộc vào trình bổ trợ Android cho Gradle được sử dụng: KotlinAndroidTarget cho các dự án sử dụng com.android.library hoặc com.android.application và KotlinMultiplatformAndroidLibraryTarget cho các dự án sử dụng com.android.kotlin.multiplatform.library.

API KotlinMultiplatformAndroidLibraryTarget được thêm vào AGP 8.8.0, vì vậy, nếu người dùng trình bổ trợ của bạn đang chạy trên phiên bản AGP thấp hơn, thì việc kiểm tra target is KotlinMultiplatformAndroidLibraryTarget có thể dẫn đến ClassNotFoundException. Để đảm bảo an toàn, hãy kiểm tra AndroidPluginVersion.getCurrent() trước khi kiểm tra loại tiêu chí nhắm mục tiêu. AndroidPluginVersion.getCurrent() yêu cầu AGP 8.3.1 trở lên.

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
                }
            }
        }
    }
}

Truy cập vào tiện ích KMP của Android và các thuộc tính của tiện ích này

Trình bổ trợ của bạn chủ yếu tương tác với tiện ích Kotlin do trình bổ trợ Kotlin Multiplatform cung cấp và tiện ích Android do AGP cung cấp cho mục tiêu Android KMP. Khối android {} trong tiện ích Kotlin trong dự án KMP được biểu thị bằng giao diện KotlinMultiplatformAndroidLibraryTarget, giao diện này cũng mở rộng KotlinMultiplatformAndroidLibraryExtension. Điều này có nghĩa là bạn có thể truy cập vào cả thuộc tính DSL dành riêng cho mục tiêu và dành riêng cho Android thông qua đối tượng duy nhất này.

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
            }
        }
    }
}

Không giống như các trình bổ trợ Android khác (chẳng hạn như com.android.library hoặc com.android.application), trình bổ trợ KMP Android không đăng ký tiện ích DSL chính ở cấp dự án. Cấu hình này nằm trong hệ phân cấp mục tiêu KMP để đảm bảo chỉ áp dụng cho mục tiêu Android cụ thể được xác định trong chế độ thiết lập đa nền tảng.

Xử lý các bản biên dịch và nhóm tài nguyên

Thông thường, các trình bổ trợ cần hoạt động ở cấp độ chi tiết hơn so với mục tiêu, cụ thể là ở cấp độ biên dịch. KotlinMultiplatformAndroidLibraryTarget chứa các thực thể KotlinMultiplatformAndroidCompilation (ví dụ: main, hostTest, deviceTest). Mỗi bản biên dịch được liên kết với các nhóm tài nguyên Kotlin. Trình bổ trợ có thể tương tác với các trình bổ trợ này để thêm nguồn, phần phụ thuộc hoặc định cấu hình các tác vụ biên dịch.

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 ->

                    }
                }
            }
        }
    }
}

Định cấu hình bản biên dịch kiểm thử trong trình bổ trợ quy ước

Khi định cấu hình giá trị mặc định cho các bản biên dịch kiểm thử (chẳng hạn như targetSdk cho kiểm thử đo lường) trong một trình bổ trợ quy ước, hãy tránh sử dụng các phương thức hỗ trợ như withDeviceTest { } hoặc withHostTest { }. Việc gọi các phương thức này sẽ kích hoạt ngay việc tạo các biến thể kiểm thử Android tương ứng và biên dịch cho mọi mô-đun áp dụng trình bổ trợ quy ước, điều này có thể không phù hợp. Bạn cũng không thể gọi các phương thức này lần thứ hai trong một mô-đun cụ thể để tinh chỉnh chế độ cài đặt, vì làm như vậy sẽ gửi trạng thái lỗi cho biết quá trình biên dịch đã được tạo.

Thay vào đó, hãy sử dụng khối configureEach phản ứng trên vùng chứa bản biên dịch. Điều này cho phép bạn cung cấp các cấu hình mặc định chỉ áp dụng nếu và khi một mô-đun bật rõ ràng quá trình biên dịch kiểm thử:

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) }
                    }
                }
        }
    }
}

Mẫu này đảm bảo rằng trình bổ trợ quy ước của bạn vẫn lười và cho phép các mô-đun riêng lẻ gọi withDeviceTest { } để bật và tuỳ chỉnh thêm các kiểm thử mà không xung đột với các giá trị mặc định.

Tương tác với Variant API

Đối với các tác vụ yêu cầu cấu hình giai đoạn muộn, quyền truy cập cấu phần phần mềm (như tệp kê khai hoặc mã byte) hoặc khả năng bật/tắt các thành phần cụ thể, bạn phải sử dụng Android Variant API. Trong các dự án KMP, tiện ích này thuộc loại KotlinMultiplatformAndroidComponentsExtension.

Phần mở rộng được đăng ký ở cấp dự án khi Plugin Android KMP được áp dụng.

Sử dụng beforeVariants để kiểm soát việc tạo biến thể hoặc các thành phần kiểm thử lồng nhau (hostTests và deviceTests). Đây là nơi thích hợp để lập trình tắt kiểm thử hoặc thay đổi giá trị thuộc tính 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 }
            }
        }
    }
}

Sử dụng onVariants để truy cập vào đối tượng biến thể cuối cùng (KotlinMultiplatformAndroidVariant). Đây là nơi bạn có thể kiểm tra các thuộc tính đã phân giải hoặc đăng ký các phép biến đổi trên cấu phần phần mềm như tệp kê khai sáp nhập hoặc các lớp thư viện.

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)
            }
        }
    }
}