Menulis plugin Gradle

Plugin Android Gradle (AGP) adalah sistem build resmi untuk aplikasi Android. Plugin ini mencakup dukungan untuk mengompilasi berbagai jenis sumber dan menautkannya ke dalam aplikasi yang dapat Anda jalankan pada perangkat Android fisik atau emulator.

AGP berisi titik ekstensi untuk plugin agar dapat mengontrol input build dan memperluas fungsinya melalui langkah-langkah baru yang dapat diintegrasikan dengan tugas build standar. Versi AGP versi sebelumnya tidak memiliki API resmi yang terpisah dengan jelas dari implementasi internal. Mulai dari versi 7.0, AGP memiliki kumpulan API resmi dan stabil yang dapat Anda andalkan.

Siklus proses AGP API

AGP mengikuti siklus proses fitur Gradle untuk menetapkan status API-nya:

  • Internal: Tidak dimaksudkan untuk penggunaan umum
  • Inkubasi: Tersedia untuk penggunaan publik, tetapi tidak bersifat final, yang berarti mungkin tidak kompatibel dengan versi akhir
  • Publik: Tersedia untuk penggunaan publik dan stabil
  • Tidak digunakan lagi: Tidak lagi didukung, dan digantikan dengan API baru

Kebijakan penghentian layanan

AGP berkembang dengan penghentian API lama dan penggantiannya dengan API baru yang stabil dan Domain Specific Language (DSL) baru. Evolusi ini akan mencakup beberapa rilis AGP, dan Anda dapat mempelajarinya lebih lanjut di linimasa migrasi AGP API/DSL.

Jika AGP API tidak digunakan lagi, untuk migrasi ini, jika tidak, API tersebut akan terus tersedia dalam rilis utama saat ini, tetapi akan menghasilkan peringatan. API yang tidak digunakan lagi akan sepenuhnya dihapus dari AGP dalam rilis utama berikutnya. Misalnya, jika API tidak digunakan lagi di AGP 7.0, API tersebut akan tersedia dalam versi tersebut dan menghasilkan peringatan. API tersebut tidak akan lagi tersedia di AGP 8.0.

Untuk melihat contoh API baru yang digunakan dalam penyesuaian build umum, lihat resep plugin Android Gradle. Resep ini memberikan contoh penyesuaian build umum. Anda juga dapat menemukan detail lebih lanjut tentang API baru di dokumentasi referensi kami.

Dasar-dasar build Gradle

Panduan ini tidak mencakup seluruh sistem build Gradle. Namun, panduan ini mencakup kumpulan konsep minimum yang diperlukan untuk membantu Anda berintegrasi dengan API kami, dan menautkan ke dokumentasi Gradle utama untuk dibaca lebih lanjut.

Kami mengasumsikan pengetahuan dasar tentang cara kerja Gradle, termasuk cara mengonfigurasi project, mengedit file build, menerapkan plugin, dan menjalankan tugas. Untuk mempelajari dasar-dasar Gradle sehubungan dengan AGP, sebaiknya tinjau Mengonfigurasi build Anda. Untuk mempelajari framework umum untuk menyesuaikan plugin Gradle, lihat Mengembangkan Plugin Gradle Kustom.

Glosarium jenis lambat Gradle

Gradle menawarkan sejumlah jenis perilaku "secara lambat", atau membantu menunda komputasi berat atau pembuatan Task ke fase build berikutnya. Jenis ini adalah inti dari banyak Gradle API dan AGP API. Daftar berikut mencakup jenis Gradle utama yang terlibat dalam eksekusi lambat, serta metode utamanya.

Provider<T>
Memberikan nilai jenis T (dengan "T" berarti jenis apa pun), yang dapat dibaca selama fase eksekusi menggunakan get() atau diubah menjadi Provider<S> baru (dengan "S" berarti jenis lainnya) menggunakan metode map(), flatMap(), dan zip(). Perhatikan bahwa get() tidak boleh dipanggil selama fase konfigurasi.
  • map(): Menerima lambda dan menghasilkan Provider jenis S, Provider<S>. Argumen lambda untuk map() mengambil nilai T dan menghasilkan nilai S. Lambda tidak dieksekusi secara langsung; sebagai gantinya, eksekusinya ditunda pada saat get() dipanggil pada Provider<S> yang dihasilkan, membuat seluruh rantai menjadi lambat.
  • flatMap(): Juga menerima lambda dan menghasilkan Provider<S>, tetapi lambda mengambil T nilai dan menghasilkan Provider<S> (bukan menghasilkan S nilai secara langsung). Gunakan flatMap() saat S tidak dapat ditentukan pada waktu konfigurasi dan Anda hanya dapat memperoleh Provider<S>. Dari sudut pandang praktis, jika Anda menggunakan map() dan mendapatkan jenis hasil Provider<Provider<S>>, itu berarti Anda harus menggunakan flatMap() sebagai gantinya.
  • zip(): Memungkinkan Anda menggabungkan dua instance Provider untuk menghasilkan Provider baru, dengan nilai yang dikomputasi menggunakan fungsi yang menggabungkan nilai dari dua instance Providers input.
Property<T>
Mengimplementasikan Provider<T>, sehingga juga memberikan nilai jenis T. Berbeda dengan Provider<T> yang bersifat hanya baca, Anda juga dapat menetapkan nilai untuk Property<T>. Ada dua cara untuk melakukannya:
  • Tetapkan nilai jenis T secara langsung saat tersedia, tanpa perlu komputasi yang ditangguhkan.
  • Tetapkan Provider<T> lainnya sebagai sumber nilai Property<T>. Dalam hal ini, nilai T hanya terwujud saat Property.get() dipanggil.
TaskProvider
Mengimplementasikan Provider<Task>. Untuk menghasilkan TaskProvider, gunakan tasks.register(), bukan tasks.create(), untuk memastikan tugas hanya dibuat instance-nya dengan lambat saat diperlukan. Anda dapat menggunakan flatMap() untuk mengakses output Task sebelum Task dibuat, yang dapat berguna jika Anda ingin menggunakan output tersebut sebagai input ke instance Task lainnya.

Penyedia dan metode transformasinya sangat penting untuk menyiapkan input dan output tugas secara lambat, yaitu, tanpa perlu membuat semua tugas terlebih dahulu dan menyelesaikan nilainya.

Penyedia juga membawa informasi dependensi tugas. Saat Anda membuat Provider dengan mengubah output Task, Task tersebut akan menjadi dependensi implisit Provider dan akan dibuat serta dijalankan setiap kali nilai Provider diselesaikan, seperti ketika Task lain memerlukannya.

Berikut adalah contoh untuk mendaftarkan dua tugas, GitVersionTask dan ManifestProducerTask, saat menunda pembuatan instance Task hingga benar-benar diperlukan. Nilai input ManifestProducerTask disetel ke Provider yang diperoleh dari output GitVersionTask, sehingga ManifestProducerTask secara implisit bergantung pada GitVersionTask.

// Register a task lazily to get its TaskProvider.
val gitVersionProvider: TaskProvider =
    project.tasks.register("gitVersionProvider", GitVersionTask::class.java) {
        it.gitVersionOutputFile.set(
            File(project.buildDir, "intermediates/gitVersionProvider/output")
        )
    }

...

/**
 * Register another task in the configuration block (also executed lazily,
 * only if the task is required).
 */
val manifestProducer =
    project.tasks.register(variant.name + "ManifestProducer", ManifestProducerTask::class.java) {
        /**
         * Connect this task's input (gitInfoFile) to the output of
         * gitVersionProvider.
         */
        it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
    }

Kedua tugas ini hanya akan dijalankan jika diminta secara eksplisit. Hal ini dapat terjadi sebagai bagian dari pemanggilan Gradle, misalnya, jika Anda menjalankan ./gradlew debugManifestProducer, atau jika output ManifestProducerTask terhubung ke beberapa tugas lain dan nilainya menjadi diperlukan.

Meskipun Anda akan menulis tugas kustom yang menggunakan input dan/atau menghasilkan output, AGP tidak menawarkan akses publik ke tugasnya sendiri secara langsung. Itu adalah detail implementasi yang dapat berubah dari versi ke versi. Sebagai gantinya, AGP menawarkan Variant API dan akses ke output tugasnya, atau artefak build, yang dapat Anda baca dan ubah. Lihat Variant API, Artefak, dan Tugas dalam dokumen ini untuk informasi selengkapnya.

Fase build Gradle

Membuat project secara inheren merupakan proses yang rumit dan membutuhkan resource, dan ada berbagai fitur seperti penghindaran konfigurasi tugas, pemeriksaan terbaru, dan fitur caching konfigurasi yang membantu meminimalkan waktu yang dihabiskan pada komputasi yang dapat direproduksi atau tidak diperlukan.

Untuk menerapkan beberapa pengoptimalan ini, skrip dan plugin Gradle harus mematuhi aturan yang ketat selama setiap fase build Gradle yang berbeda: inisialisasi, konfigurasi, dan eksekusi. Dalam panduan ini, kita akan berfokus pada fase konfigurasi dan eksekusi. Anda dapat menemukan informasi selengkapnya tentang semua fase dalam panduan siklus proses build Gradle.

Fase konfigurasi

Selama fase konfigurasi, skrip build untuk semua project yang merupakan bagian dari build dievaluasi, plugin akan diterapkan, dan dependensi build akan diselesaikan. Fase ini harus digunakan untuk mengonfigurasi build menggunakan objek DSL dan untuk mendaftarkan tugas dan inputnya dengan lambat.

Karena fase konfigurasi selalu berjalan, terlepas dari tugas yang diminta untuk berjalan, penting untuk menjaganya tetap ramping dan membatasi komputasi apa pun agar tidak bergantung pada input selain skrip build itu sendiri. Artinya, sebaiknya Anda tidak menjalankan program eksternal atau membaca dari jaringan, atau melakukan komputasi panjang yang dapat ditunda ke fase eksekusi sebagai instance Task yang tepat.

Fase eksekusi

Pada fase eksekusi, tugas yang diminta dan tugas dependennya akan dieksekusi. Secara khusus, metode class Task yang ditandai dengan @TaskAction akan dieksekusi. Selama eksekusi tugas, Anda diizinkan untuk membaca dari input (seperti file) dan menyelesaikan penyedia lambat dengan memanggil Provider<T>.get(). Menyelesaikan penyedia lambat dengan cara ini akan memulai urutan panggilan map() atau flatMap() yang mengikuti informasi dependensi tugas yang ada dalam penyedia. Tugas berjalan dengan lambat untuk mewujudkan nilai yang diperlukan.

Variant API, Artefak, dan Tugas

Variant API adalah mekanisme ekstensi di plugin Android Gradle yang memungkinkan Anda memanipulasi berbagai opsi, yang biasanya ditetapkan menggunakan DSL dalam file konfigurasi build, yang memengaruhi build Android. Variant API juga memberi Anda akses ke artefak perantara dan final yang dibuat oleh build, seperti file class, manifes gabungan, atau file APK/AAB.

Alur build dan titik ekstensi Android

Saat berinteraksi dengan AGP, gunakan titik ekstensi yang dibuat khusus, bukan mendaftarkan callback siklus proses Gradle standar (seperti afterEvaluate()) atau menyiapkan dependensi Task eksplisit. Tugas yang dibuat oleh AGP dianggap detail implementasi dan tidak ditampilkan sebagai API publik. Anda sebaiknya jangan mencoba mendapatkan instance objek Task atau menebak nama Task dan menambahkan callback atau dependensi ke objek Task tersebut secara langsung.

AGP menyelesaikan langkah-langkah berikut untuk membuat dan menjalankan instance Task, yang pada akhirnya menghasilkan artefak build. Langkah utama yang terlibat dalam pembuatan objek Variant diikuti dengan callback yang memungkinkan Anda membuat perubahan pada objek tertentu yang dibuat sebagai bagian dari build. Penting untuk diperhatikan bahwa semua callback terjadi selama fase konfigurasi (dijelaskan di halaman ini) dan harus berjalan cepat. Oleh karena itu, sebagai gantinya, pekerjaan yang rumit untuk instance Task yang tepat selama fase eksekusi akan ditunda.

  1. Penguraian DSL: Ini adalah saat skrip build dievaluasi, dan saat berbagai properti objek Android DSL dari blok android dibuat dan disetel. Callback Variant API yang dijelaskan di bagian berikut juga terdaftar selama fase ini.
  2. finalizeDsl(): Callback yang memungkinkan Anda mengubah objek DSL sebelum dikunci untuk pembuatan komponen (varian). Objek VariantBuilder dibuat berdasarkan data yang dimuat dalam objek DSL.

  3. Penguncian DSL: DSL kini dikunci dan tidak lagi dapat diubah.

  4. beforeVariants(): Callback ini dapat memengaruhi komponen yang dibuat, dan beberapa propertinya, melalui VariantBuilder. Callback masih memungkinkan modifikasi pada alur build dan artefak yang dihasilkan.

  5. Pembuatan varian: Daftar komponen dan artefak yang akan dibuat kini telah diselesaikan dan tidak dapat diubah.

  6. onVariants(): Dalam callback ini, Anda mendapatkan akses ke objek Variant yang dibuat dan Anda dapat menetapkan nilai atau penyedia untuk nilai Property yang dimuatnya agar dikomputasi dengan lambat.

  7. Penguncian varian: Objek varian kini dikunci dan tidak lagi dapat diubah.

  8. Tugas yang dibuat: Objek Variant beserta nilai Property-nya digunakan untuk membuat instance Task yang diperlukan untuk menjalankan build.

AGP memperkenalkan AndroidComponentsExtension, yang memungkinkan Anda mendaftarkan callback untuk finalizeDsl(), beforeVariants(), dan onVariants(). Ekstensi ini tersedia dalam skrip build melalui blok androidComponents:

// This is used only for configuring the Android build through DSL.
android { ... }

// The androidComponents block is separate from the DSL.
androidComponents {
   finalizeDsl { extension ->
      ...
   }
}

Namun, sebaiknya simpan skrip build hanya untuk konfigurasi deklaratif menggunakan DSL blok Android dan pindahkan logika imperatif kustom ke buildSrc atau plugin eksternal. Anda juga dapat melihat contoh buildSrc di repositori GitHub urutan langkah Gradle untuk mempelajari cara membuat plugin dalam project Anda. Berikut adalah contoh pendaftaran callback dari kode plugin:

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            ...
        }
    }
}

Mari ketahui lebih lanjut callback yang tersedia dan jenis kasus penggunaan yang dapat didukung oleh plugin Anda di setiap callback:

finalizeDsl(callback: (DslExtensionT) -> Unit)

Dalam callback ini, Anda dapat mengakses dan mengubah objek DSL yang dibuat dengan menguraikan informasi dari blok android dalam file build. Objek DSL ini akan digunakan untuk melakukan inisialisasi dan mengonfigurasi varian dalam fase build selanjutnya. Misalnya, Anda dapat membuat konfigurasi baru atau mengganti properti secara terprogram—tetapi perlu diingat bahwa semua nilai harus diselesaikan pada waktu konfigurasi, sehingga nilai tersebut tidak boleh bergantung pada input eksternal. Setelah callback ini selesai dijalankan, objek DSL tidak lagi berguna dan Anda tidak boleh lagi menyimpan referensi ke objek tersebut atau memodifikasi nilainya.

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            extension.buildTypes.create("extra").let {
                it.isJniDebuggable = true
            }
        }
    }
}

beforeVariants()

Pada tahap build ini, Anda mendapatkan akses ke objek VariantBuilder, yang menentukan varian yang akan dibuat dan propertinya. Misalnya, Anda dapat secara terprogram menonaktifkan varian tertentu, pengujiannya, atau mengubah nilai properti (misalnya, minSdk) hanya untuk varian yang dipilih. Mirip dengan finalizeDsl(), semua nilai yang Anda berikan harus diselesaikan pada waktu konfigurasi dan tidak bergantung pada input eksternal. Objek VariantBuilder tidak boleh diubah setelah eksekusi callback beforeVariants() selesai.

androidComponents {
    beforeVariants { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

Callback beforeVariants() secara opsional mengambil VariantSelector, yang dapat Anda peroleh melalui metode selector() padaandroidComponentsExtension. Anda dapat menggunakannya untuk memfilter komponen yang berpartisipasi dalam pemanggilan callback berdasarkan nama, jenis build, atau ragam produknya.

androidComponents {
    beforeVariants(selector().withName("adfree")) { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

onVariants()

Saat onVariants() dipanggil, semua artefak yang akan dibuat oleh AGP sudah ditentukan, sehingga Anda tidak dapat menonaktifkannya lagi. Namun, Anda dapat mengubah beberapa nilai yang digunakan untuk tugas dengan menetapkannya untuk atribut Property dalam objek Variant. Karena nilai Property hanya akan diselesaikan ketika tugas AGP dijalankan, Anda dapat menghubungkannya dengan aman ke penyedia dari tugas kustom Anda sendiri yang akan melakukan komputasi apa pun yang diperlukan, termasuk membaca dari input eksternal seperti file atau jaringan.

// onVariants also supports VariantSelectors:
onVariants(selector().withBuildType("release")) { variant ->
    // Gather the output when we are in single mode (no multi-apk).
    val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE }

    // Create version code generating task
    val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) {
        it.outputFile.set(project.layout.buildDirectory.file("${variant.name}/versionCode.txt"))
    }
    /**
     * Wire version code from the task output.
     * map() will create a lazy provider that:
     * 1. Runs just before the consumer(s), ensuring that the producer
     * (VersionCodeTask) has run and therefore the file is created.
     * 2. Contains task dependency information so that the consumer(s) run after
     * the producer.
     */
    mainOutput.versionCode.set(versionCodeTask.map { it.outputFile.get().asFile.readText().toInt() })
}

Memberikan kontribusi pada sumber yang dihasilkan ke build

Plugin Anda dapat berkontribusi pada beberapa jenis sumber yang dihasilkan, seperti:

Untuk mengetahui daftar lengkap sumber yang dapat Anda tambahkan, lihat Sources API.

Cuplikan kode ini menunjukkan cara menambahkan folder sumber kustom yang bernama ${variant.name} ke set sumber Java menggunakan fungsi addStaticSourceDirectory(). Selanjutnya, toolchain Android akan memproses folder ini.

onVariants { variant ->
    variant.sources.java?.let { java ->
        java.addStaticSourceDirectory("custom/src/kotlin/${variant.name}")
    }
}

Lihat urutan langkah addJavaSource untuk mengetahui detail selengkapnya.

Cuplikan kode ini menunjukkan cara menambahkan direktori dengan resource Android yang dihasilkan dari tugas kustom ke set sumber res. Proses ini mirip dengan jenis sumber lainnya.

onVariants(selector().withBuildType("release")) { variant ->
    // Step 1. Register the task.
    val resCreationTask =
       project.tasks.register<ResCreatorTask>("create${variant.name}Res")

    // Step 2. Register the task output to the variant-generated source directory.
    variant.sources.res?.addGeneratedSourceDirectory(
       resCreationTask,
       ResCreatorTask::outputDirectory)
    }

...

// Step 3. Define the task.
abstract class ResCreatorTask: DefaultTask() {
   @get:OutputFiles
   abstract val outputDirectory: DirectoryProperty

   @TaskAction
   fun taskAction() {
      // Step 4. Generate your resources.
      ...
   }
}

Lihat urutan langkah addCustomAsset untuk mengetahui detail selengkapnya.

Mengakses dan mengubah artefak

Selain memungkinkan Anda mengubah properti sederhana pada objek Variant, AGP juga berisi mekanisme ekstensi yang memungkinkan Anda membaca atau mengubah artefak perantara dan final yang dihasilkan selama proses build. Misalnya, Anda dapat membaca konten file AndroidManifest.xml final yang digabungkan dalam Task kustom untuk menganalisisnya, atau Anda dapat sepenuhnya mengganti kontennya dengan file manifes yang dihasilkan oleh Task kustom Anda.

Anda dapat menemukan daftar artefak yang saat ini didukung dalam dokumentasi referensi untuk class Artifact. Setiap jenis artefak memiliki properti tertentu yang akan sangat bermanfaat:

Kardinalitas

Kardinalitas Artifact mewakili jumlah instance FileSystemLocation, atau jumlah file atau direktori jenis artefak. Anda dapat memperoleh informasi tentang kardinalitas artefak dengan memeriksa class induknya: Artefak dengan satu FileSystemLocation akan menjadi subclass Artifact.Single; artefak dengan beberapa instance FileSystemLocation akan menjadi subclass Artifact.Multiple.

Jenis FileSystemLocation

Anda dapat memeriksa apakah Artifact mewakili file atau direktori dengan melihat jenis FileSystemLocation yang diparameterisasinya, yang dapat berupa RegularFile atau Directory.

Operasi yang didukung

Setiap class Artifact dapat mengimplementasikan salah satu antarmuka berikut untuk menunjukkan operasi mana yang didukungnya:

  • Transformable: Memungkinkan Artifact untuk digunakan sebagai input ke Task yang melakukan transformasi arbitrer di dalamnya dan meng-output versi baru Artifact.
  • Appendable: Hanya berlaku untuk artefak yang merupakan subclass Artifact.Multiple. Ini berarti Artifact dapat ditambahkan, yaitu, Task kustom dapat membuat instance baru dari jenis Artifact ini yang akan ditambahkan ke daftar yang ada.
  • Replaceable: Hanya berlaku untuk artefak yang merupakan subclass Artifact.Single. Artifact yang dapat diganti dapat diganti dengan instance yang benar-benar baru, yang dihasilkan sebagai output Task.

Selain tiga operasi modifikasi artefak, setiap artefak mendukung operasi get() (atau getAll()), yang menampilkan Provider dengan versi final artefak (setelah semua operasi di dalamnya selesai).

Beberapa plugin dapat menambahkan sejumlah operasi pada artefak ke dalam pipeline dari callback onVariants(), dan AGP akan memastikan semuanya dirantai dengan benar sehingga semua tugas berjalan pada waktu yang tepat dan artefak dihasilkan dengan benar dan diperbarui. Ini berarti saat operasi mengubah output dengan menambahkan, mengganti, atau mengubahnya, operasi berikutnya akan melihat artefak dalam versi terbaru sebagai input, dan sebagainya.

Titik entri ke operasi pendaftaran adalah class Artifacts. Cuplikan kode berikut menunjukkan cara Anda dapat mengakses instance Artifacts dari properti di objek Variant dalam callback onVariants().

Kemudian, Anda dapat meneruskan TaskProvider kustom untuk mendapatkan objek TaskBasedOperation (1), dan menggunakannya untuk menghubungkan input dan outputnya menggunakan salah satu metode wiredWith* (2).

Metode tepat yang harus Anda pilih bergantung pada kardinalitas dan jenis FileSystemLocation yang diimplementasikan oleh Artifact yang ingin Anda ubah.

Terakhir, Anda meneruskan jenis Artifact ke metode yang mewakili operasi yang dipilih pada objek *OperationRequest yang Anda dapatkan sebagai gantinya, misalnya, toAppendTo(), toTransform() , atau toCreate() (3).

androidComponents.onVariants { variant ->
    val manifestUpdater = // Custom task that will be used for the transform.
            project.tasks.register(variant.name + "ManifestUpdater", ManifestTransformerTask::class.java) {
                it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
            }
    // (1) Register the TaskProvider w.
    val variant.artifacts.use(manifestUpdater)
         // (2) Connect the input and output files.
        .wiredWithFiles(
            ManifestTransformerTask::mergedManifest,
            ManifestTransformerTask::updatedManifest)
        // (3) Indicate the artifact and operation type.
        .toTransform(SingleArtifact.MERGED_MANIFEST)
}

Dalam contoh ini, MERGED_MANIFEST adalah SingleArtifact, dan merupakan RegularFile. Karena itu, kita perlu menggunakan metode wiredWithFiles, yang menerima satu referensi RegularFileProperty untuk input, dan satu RegularFileProperty untuk output. Ada metode wiredWith* lain di class TaskBasedOperation yang akan berfungsi untuk kombinasi lain dari jenis kardinalitas Artifact dan FileSystemLocation.

Untuk mempelajari lebih lanjut cara memperluas AGP, sebaiknya baca bagian berikut dari panduan sistem build Gradle: