Membuat grafik secara terprogram menggunakan DSL Kotlin

Komponen Navigasi menyediakan bahasa khusus domain, atau DSL, berbasis Kotlin yang bergantung pada type-safe builder Kotlin. API ini memungkinkan Anda membuat grafik secara deklaratif di kode Kotlin, bukan di dalam resource XML. Cara ini dapat berguna jika Anda ingin membuat navigasi aplikasi secara dinamis. Misalnya, aplikasi Anda dapat mendownload dan meng-cache konfigurasi navigasi dari layanan web eksternal, lalu menggunakan konfigurasi tersebut untuk membuat grafik navigasi secara dinamis dalam fungsi onCreate() aktivitas Anda.

Dependensi

Untuk menggunakan DSL Kotlin, tambahkan dependensi berikut ke file build.gradle aplikasi Anda:

Groovy

dependencies {
    def nav_version = "2.5.3"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.5.3"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

Membuat grafik

Mari kita mulai dengan contoh sederhana berdasarkan aplikasi Sunflower. Untuk contoh ini, kita memiliki dua tujuan: home dan plant_detail. Tujuan home ada saat pengguna pertama kali meluncurkan aplikasi. Tujuan ini menampilkan daftar tanaman dari taman pengguna. Saat pengguna memilih salah satu tanaman, aplikasi akan menavigasi ke tujuan plant_detail.

Gambar 1 menunjukkan tujuan berikut beserta argumen yang diperlukan oleh tujuan plant_detail dan tindakan to_plant_detail yang digunakan aplikasi untuk menavigasi dari home ke plant_detail.

Aplikasi Sunflower memiliki dua tujuan bersama dengan tindakan yang menghubungkannya.
Gambar 1. Aplikasi Sunflower memiliki dua tujuan, home dan plant_detail, bersama dengan tindakan yang menghubungkan keduanya.

Membuat host untuk grafik navigasi DSL Kotlin

Terlepas dari cara membuat grafik, Anda perlu menghosting grafik dalam NavHost. Sunflower menggunakan fragmen, jadi mari kita menggunakan NavHostFragment di dalam FragmentContainerView, seperti yang ditunjukkan pada contoh berikut:

<!-- activity_garden.xml -->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

Perhatikan bahwa atribut app:navGraph tidak ditetapkan dalam contoh ini, karena grafik dibuat secara terprogram dan bukan ditetapkan sebagai resource XML.

Membuat konstanta untuk grafik

Saat menangani grafik navigasi berbasis XML, proses build Android menguraikan file resource grafik dan menentukan konstanta numerik untuk setiap atribut id yang ditentukan dalam grafik. Konstanta ini dapat diakses dalam kode Anda melalui class resource yang dihasilkan, R.id.

Misalnya, cuplikan grafik XML berikut mendeklarasikan tujuan fragmen dengan id, home:

<navigation ...>
   <fragment android:id="@+id/home" ... />
   ...
</navigation>

Proses build menghasilkan nilai konstanta R.id.home yang dikaitkan dengan tujuan ini. Selanjutnya, Anda dapat mereferensikan tujuan tersebut dari kode Anda dengan menggunakan nilai konstanta ini.

Proses penguraian dan pembuatan konstanta tidak terjadi saat Anda membuat grafik secara terprogram menggunakan DSL Kotlin. Sebaliknya, Anda harus menentukan konstanta Anda sendiri untuk setiap tujuan, tindakan, dan argumen yang memiliki nilai id. Setiap ID harus unik dan konsisten di seluruh perubahan konfigurasi.

Satu cara yang teratur untuk membuat konstanta adalah membuat set bertingkat object Kotlin yang menentukan konstanta secara statis, seperti yang ditunjukkan pada contoh berikut:

object nav_graph {

    const val id = 1 // graph id

    object dest {
        const val home = 2
        const val plant_detail = 3
    }

    object action {
        const val to_plant_detail = 4
    }

    object args {
        const val plant_id = "plantId"
    }
}

Dengan struktur ini, Anda dapat mengakses nilai ID dalam kode dengan menautkan panggilan objek secara bersamaan, seperti yang ditunjukkan pada contoh berikut:

nav_graph.id                     // graph id
nav_graph.dest.home              // home destination id
nav_graph.action.to_plant_detail // action home -> plant_detail id
nav_graph.args.plant_id          // destination argument name

Setelah menentukan kumpulan awal ID, Anda dapat membuat grafik navigasi. Gunakan fungsi ekstensi NavController.createGraph() untuk membuat NavGraph, yang meneruskan id untuk grafik Anda, nilai ID untuk startDestination, dan lambda di akhir yang menentukan struktur grafik Anda.

Anda dapat membuat grafik pada fungsi onCreate() aktivitas Anda. createGraph() menampilkan Navgraph yang kemudian dapat Anda tetapkan ke properti graph dari NavController yang terkait dengan NavHost, seperti yang ditunjukkan pada contoh berikut:

class GardenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_garden)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.navController.apply {
            graph = createGraph(nav_graph.id, nav_graph.dest.home) {
                fragment<HomeViewPagerFragment>(nav_graph.dest.home) {
                    label = getString(R.string.home_title)
                    action(nav_graph.action.to_plant_detail) {
                        destinationId = nav_graph.dest.plant_detail
                    }
                }
                fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
                    label = getString(R.string.plant_detail_title)
                    argument(nav_graph.args.plant_id) {
                        type = NavType.StringType
                    }
                }
            }
        }
    }
}

Dalam contoh ini, lambda di akhir mendefinisikan dua tujuan fragmen menggunakan fungsi builder DSL fragment(). Fungsi ini memerlukan ID untuk tujuan. Fungsi tersebut juga menerima lambda opsional untuk konfigurasi tambahan, seperti tujuan label, serta fungsi builder tersemat untuk tindakan, argumen, dan deep link.

Class Fragment yang mengelola UI setiap tujuan diteruskan sebagai jenis yang diparameterisasi dalam tanda kurung sudut (<>). Ini memiliki efek yang sama seperti menetapkan atribut android:name pada tujuan fragmen yang ditentukan menggunakan XML.

Setelah membuat dan menetapkan grafik, Anda dapat menavigasi dari home ke plant_detail menggunakan NavController.navigate(), seperti yang ditunjukkan pada contoh berikut:

private fun navigateToPlant(plantId: String) {

    val args = bundleOf(nav_graph.args.plant_id to plantId)

    findNavController().navigate(nav_graph.action.to_plant_detail, args)
}

Jenis tujuan yang didukung

DSL Kotlin mendukung tujuan Fragment, Activity, dan NavGraph, masing-masing memiliki fungsi ekstensi inline sendiri yang tersedia untuk membuat dan mengonfigurasi tujuan.

Tujuan fragmen

Fungsi DSL fragment() dapat diparameterisasi untuk class Fragment yang mengimplementasi. Fungsi tersebut memerlukan ID unik untuk ditetapkan ke tujuan ini bersama dengan lambda tempat Anda dapat memberikan konfigurasi tambahan.

fragment<FragmentDestination>(nav_graph.dest.fragment_dest_id) {
   label = getString(R.string.fragment_title)
   // arguments, actions, deepLinks...
}

Tujuan aktivitas

Fungsi DSL activity() memerlukan ID unik untuk ditetapkan ke tujuan ini, tetapi tidak diparameterisasi ke class yang mengimplementasi aktivitas apa pun. Sebaliknya, Anda dapat menetapkan activityClass opsional pada lambda di akhir. Fleksibilitas ini memungkinkan Anda menentukan tujuan aktivitas untuk aktivitas yang diluncurkan dari intent implisit, apabila class aktivitas eksplisit tidak memungkinkan. Seperti tujuan fragmen, Anda juga dapat menentukan serta mengonfigurasi label dan argumen apa pun.

activity(nav_graph.dest.activity_dest_id) {
    label = getString(R.string.activity_title)
    // arguments, actions, deepLinks...

    activityClass = ActivityDestination::class
}

Anda dapat menggunakan fungsi DSL navigation() untuk membuat grafik navigasi bertingkat. Seperti jenis tujuan lainnya, fungsi DSL ini membutuhkan tiga argumen: ID untuk ditetapkan ke grafik, ID tujuan awal untuk grafik, dan lambda untuk mengonfigurasi grafik lebih lanjut. Elemen yang valid untuk lambda mencakup argumen, tindakan, tujuan lain, deep link, dan label.

navigation(nav_graph.dest.nav_graph_dest, nav_graph.dest.start_dest) {
   // label, arguments, actions, other destinations, deep links
}

Mendukung tujuan kustom

Anda dapat menggunakan addDestination() untuk menambahkan jenis tujuan kustom ke DSL Kotlin Anda yang tidak didukung secara default, seperti yang ditunjukkan pada contoh berikut:

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}
addDestination(customDestination)

Anda juga dapat menggunakan operator plus unary (+) untuk menambahkan tujuan yang baru dibuat langsung ke grafik:

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}

Menyediakan argumen tujuan

Anda dapat menentukan argumen opsional atau wajib untuk jenis tujuan apa pun. Untuk menentukan argumen, panggil fungsi argument() pada NavDestinationBuilder, yaitu class dasar untuk semua jenis builder tujuan. Fungsi ini mengambil nama argumen sebagai String dan lambda yang dapat Anda gunakan untuk membuat dan mengonfigurasi NavArgument. Di dalam lambda, Anda dapat menentukan jenis data argumen, nilai default jika berlaku, dan apakah nilai argumen dapat menjadi null.

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_name)
        nullable = true  // default false
    }
}

Jika defaultValue diberikan, maka type bersifat opsional. Dalam hal ini, jika tidak ada type yang ditentukan, jenis akan disimpulkan dari defaultValue. Jika defaultValue dan type tersedia, jenisnya harus cocok. Untuk daftar lengkap jenis argumen, lihat NavType.

Tindakan

Anda dapat menentukan tindakan dalam tujuan apa pun, termasuk tindakan global dalam grafik navigasi root. Untuk menentukan tindakan, gunakan fungsi NavDestinationBuilder.action(), dengan memberikan ID ke fungsi dan lambda untuk menyediakan konfigurasi tambahan.

Contoh berikut membuat tindakan dengan destinationId, animasi transisi, dan perilaku pop serta single-top.

action(nav_graph.action.to_plant_detail) {
    destinationId = nav_graph.dest.plant_detail
    navOptions {
        anim {
            enter = R.anim.nav_default_enter_anim
            exit = R.anim.nav_default_exit_anim
            popEnter = R.anim.nav_default_pop_enter_anim
            popExit = R.anim.nav_default_pop_exit_anim
        }
        popUpTo(nav_graph.dest.start_dest) {
            inclusive = true // default false
        }
        // if popping exclusively, you can specify popUpTo as
        // a property. e.g. popUpTo = nav_graph.dest.start_dest
        launchSingleTop = true // default false
    }
}

Deep link

Anda dapat menambahkan deep link ke tujuan apa pun, sama seperti grafik navigasi berbasis XML. Prosedur yang sama seperti yang ditentukan dalam Membuat deep link untuk tujuan berlaku untuk proses pembuatan deep link eksplisit menggunakan DSL Kotlin.

Namun, saat membuat deep link implisit, Anda tidak memiliki resource navigasi XML yang dapat dianalisis untuk elemen <deepLink>. Oleh karena itu, Anda tidak dapat mengandalkan penempatan elemen <nav-graph> di file AndroidManifest.xml dan harus secara manual menambahkan filter intent ke aktivitas Anda. Filter intent yang Anda sediakan harus cocok dengan pola URL dasar dari deep link aplikasi Anda.

Untuk setiap tujuan yang diberi deep link satu per satu, Anda dapat menyediakan pola URI yang lebih spesifik menggunakan fungsi DSL deepLink(). Fungsi ini menerima String untuk pola URI, seperti yang ditunjukkan pada contoh berikut:

deepLink("http://www.example.com/plants/")

Tidak ada batasan jumlah URI deep link yang dapat Anda tambahkan. Setiap panggilan ke deepLink() akan menambahkan deep link baru ke daftar internal yang spesifik untuk tujuan tersebut.

Berikut adalah skenario deep link implisit yang lebih kompleks yang juga menentukan parameter berbasis jalur dan kueri:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    deepLink("${baseUri}/{id}")
    deepLink("${baseUri}/{id}?name={plant_name}")
    argument(nav_graph.args.plant_id) {
       type = NavType.IntType
    }
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        nullable = true
    }
}

Perhatikan bahwa jenis interpolasi string dapat digunakan untuk menyederhanakan definisi.

Membuat ID

Library Navigasi mewajibkan nilai ID yang digunakan untuk elemen grafik adalah bilangan bulat unik yang tetap konstan selama perubahan konfigurasi. Satu cara untuk membuat ID ini adalah dengan menentukannya sebagai konstanta statis seperti yang ditunjukkan pada Membuat konstanta untuk grafik Anda. Anda juga dapat menentukan ID resource statis dalam XML sebagai resource. Atau, Anda dapat membuat ID secara dinamis. Misalnya, Anda dapat membuat penghitung urutan yang bertambah setiap kali Anda mereferensikannya.

object nav_graph {
    // Counter for id's. First ID will be 1.
    var id_counter = 1

    val id = id_counter++

    object dest {
       val home = id_counter++
       val plant_detail = id_counter++
    }

    object action {
       val to_plant_detail = id_counter++
    }

    object args {
       const val plant_id = "plantId"
    }
}

Batasan

  • Plugin Safe Args tidak kompatibel dengan Kotlin DSL, karena plugin tersebut mencari file resource XML untuk menghasilkan class Directions dan Arguments.