Fragment และ Kotlin DSL

ส่วนประกอบการนําทางมีภาษาเฉพาะโดเมนหรือ DSL ที่ใช้ Kotlin ซึ่งอาศัยตัวสร้างที่ปลอดภัยจากประเภทของ Kotlin API นี้ช่วยให้คุณเขียนกราฟในโค้ด Kotlin ได้อย่างชัดเจนแทน มากกว่าภายในทรัพยากร XML ซึ่งจะเป็นประโยชน์หากคุณต้องการสร้างแอป การนำทางแบบไดนามิก เช่น แอปอาจดาวน์โหลดและแคชการกำหนดค่าการนําทางจากเว็บเซอร์วิสภายนอก จากนั้นใช้การกําหนดค่านั้นเพื่อสร้างกราฟการนําทางแบบไดนามิกในฟังก์ชันonCreate()ของกิจกรรม

ทรัพยากร Dependency

หากต้องการใช้ Kotlin DSL กับ Fragments ให้เพิ่มทรัพยากร Dependency ต่อไปนี้ในส่วน build.gradle ไฟล์:

Groovy

dependencies {
    def nav_version = "2.9.2"

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

Kotlin

dependencies {
    val nav_version = "2.9.2"

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

การสร้างกราฟ

ต่อไปนี้เป็นตัวอย่างพื้นฐานที่อิงตามแอป Sunflower ในตัวอย่างนี้เรามีปลายทาง 2 แห่ง ได้แก่ home และ plant_detail home ปลายทาง จะแสดงเมื่อผู้ใช้เปิดแอปเป็นครั้งแรก จุดหมายนี้ แสดงรายการต้นไม้จากสวนของผู้ใช้ เมื่อผู้ใช้เลือกพืชชนิดใดชนิดหนึ่ง แอปจะไปยังปลายทาง plant_detail

รูปที่ 1 แสดงปลายทางเหล่านี้พร้อมกับอาร์กิวเมนต์ที่แอตทริบิวต์ ปลายทาง plant_detail และการดำเนินการ to_plant_detail ที่แอปใช้ เพื่อนำทางจาก home ไป plant_detail

แอป Sunflower มีจุดหมาย 2 แห่ง รวมถึงการดำเนินการที่
            เพื่อเชื่อมโยงสิ่งต่างๆ เข้าด้วยกัน
รูปที่ 1 แอป Sunflower มี 2 จุดหมาย home และ plant_detail รวมถึงการดำเนินการที่ เพื่อเชื่อมโยงสิ่งต่างๆ เข้าด้วยกัน

โฮสติ้ง Kotlin DSL Nav Graph

ก่อนที่คุณจะสร้างกราฟการนำทางของแอปได้ คุณต้องมีสถานที่เพื่อโฮสต์ กราฟ ตัวอย่างนี้ใช้ Fragment จึงโฮสต์กราฟใน NavHostFragment ภายใน FragmentContainerView

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

โปรดสังเกตว่าไม่มีการตั้งค่าแอตทริบิวต์ app:navGraph ในตัวอย่างนี้ กราฟไม่ได้กําหนดเป็นทรัพยากรในโฟลเดอร์ res/navigation จึงต้องตั้งค่าให้เป็นส่วนหนึ่งของกระบวนการ onCreate() ในกิจกรรม

ใน XML การทำงานจะเชื่อมโยงรหัสปลายทางเข้ากับอาร์กิวเมนต์อย่างน้อย 1 รายการ แต่เมื่อใช้ DSL การนำทาง เส้นทางอาจมีอาร์กิวเมนต์เป็นส่วนหนึ่งของ เส้นทาง ซึ่งหมายความว่าจะไม่มีแนวคิดเกี่ยวกับการดำเนินการเมื่อใช้ DSL

ขั้นตอนถัดไปคือกําหนดเส้นทางที่จะใช้เมื่อกําหนดกราฟ

สร้างเส้นทางสำหรับกราฟของคุณ

กราฟการนำทางแบบ XML ถูกแยกวิเคราะห์เป็นส่วนหนึ่งของ ของกระบวนการบิลด์ของ Android ระบบจะสร้างค่าคงที่ที่เป็นตัวเลขสำหรับid แอตทริบิวต์แต่ละรายการที่กําหนดไว้ในกราฟ รหัสแบบคงที่ที่สร้างขึ้นในเวลาบิลด์เหล่านี้ไม่ได้ พร้อมใช้งานเมื่อสร้างกราฟการนำทางขณะรันไทม์ เพื่อให้ DSL การนำทาง ใช้ ซีเรียลได้ ประเภท แทนที่จะเป็น รหัส แต่ละเส้นทางจะแสดงด้วยประเภทที่ไม่ซ้ำกัน

เมื่อจัดการกับอาร์กิวเมนต์ ระบบจะฝังอาร์กิวเมนต์ไว้ในประเภทเส้นทาง ซึ่งช่วยให้คุณมีความปลอดภัยของประเภทสำหรับอาร์กิวเมนต์การนําทาง

@Serializable data object Home
@Serializable data class Plant(val id: String)

เมื่อกำหนดเส้นทางแล้ว คุณสามารถสร้างกราฟการนำทางได้

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = Home
) {
    fragment<HomeFragment, Home> {
        label = resources.getString(R.string.home_title)
    }
    fragment<PlantDetailFragment, PlantDetail> {
        label = resources.getString(R.string.plant_detail_title)
    }
}

ในตัวอย่างนี้ เราได้กำหนดปลายทาง Fragment 2 รายการโดยใช้ fragment() ฟังก์ชันเครื่องมือสร้าง DSL ฟังก์ชันนี้ต้องใช้อาร์กิวเมนต์ 2 ประเภท

อันดับแรกคือคลาส Fragment ที่แสดง UI สําหรับปลายทางนี้ การตั้งค่านี้มีผลเหมือนกับการตั้งค่าแอตทริบิวต์ android:name ในปลายทางของข้อมูลโค้ดที่กําหนดโดยใช้ XML

อย่างที่ 2 คือเส้นทาง ต้องเป็นประเภทที่ serialize ได้ซึ่งขยายมาจาก Any ไฟล์นี้ควรมีอาร์กิวเมนต์การนำทางที่ปลายทางนี้จะใช้งานและประเภทของอาร์กิวเมนต์

นอกจากนี้ ฟังก์ชันยังยอมรับ Lambda ที่ไม่บังคับสําหรับการกําหนดค่าเพิ่มเติม เช่น ป้ายกํากับปลายทาง รวมถึงฟังก์ชันเครื่องมือสร้างที่ฝังสําหรับอาร์กิวเมนต์และ Deep Link ที่กําหนดเอง

สุดท้าย คุณสามารถไปยังส่วนต่างๆ จาก home ไปยัง plant_detail โดยใช้คำสั่ง NavController.navigate() ดังนี้

private fun navigateToPlant(plantId: String) {
   findNavController().navigate(route = PlantDetail(id = plantId))
}

ใน PlantDetailFragment คุณสามารถรับอาร์กิวเมนต์การนำทางได้โดยรับ NavBackStackEntry ในปัจจุบัน และเรียกใช้ toRoute กับ NavBackStackEntry เพื่อรับอินสแตนซ์เส้นทาง

val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

หาก PlantDetailFragment ใช้ ViewModel ให้รับอินสแตนซ์เส้นทางโดยใช้ SavedStateHandle.toRoute

val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

ส่วนที่เหลือของคู่มือนี้จะอธิบายองค์ประกอบกราฟการไปยังส่วนต่างๆ, ปลายทาง, และวิธีใช้เมื่อสร้างกราฟ

จุดหมาย

DSL ของ Kotlin รองรับปลายทาง 3 ประเภทในตัว ได้แก่ ปลายทาง Fragment, Activity และ NavGraph ซึ่งแต่ละประเภทมีฟังก์ชันส่วนขยายแบบอินไลน์ของตัวเองสําหรับการสร้างและกำหนดค่าปลายทาง

ปลายทางของ Fragment

fragment() ฟังก์ชัน DSL สามารถทำเป็นพารามิเตอร์ได้ด้วยคลาส Fragment สำหรับ UI และ ประเภทเส้นทางที่ใช้ระบุจุดหมายนี้โดยไม่ซ้ำกัน ตามด้วยแลมบ์ดา ซึ่งคุณสามารถกำหนดค่าเพิ่มเติมตามที่อธิบายไว้ในการนำทาง ด้วยส่วนกราฟ Kotlin DSL

fragment<MyFragment, MyRoute> {
   label = getString(R.string.fragment_title)
   // custom argument types, deepLinks
}

จุดหมายของกิจกรรม

activity() ฟังก์ชัน DSL ใช้พารามิเตอร์ประเภทสำหรับเส้นทาง แต่ไม่ได้แปลงเป็นพารามิเตอร์ ประเภทกิจกรรมที่ติดตั้งใช้งาน แต่คุณตั้งค่า activityClass ที่ไม่บังคับในแลมดาต่อท้ายแทน ความยืดหยุ่นนี้ช่วยให้คุณกําหนดปลายทางของกิจกรรมสําหรับกิจกรรมที่ควรเปิดโดยใช้Intent ที่ไม่ชัด ซึ่งคลาสกิจกรรมที่ชัดเจนจะไม่เหมาะ เช่นเดียวกับปลายทางของส่วนย่อย คุณยังสามารถ กำหนดค่าป้ายกำกับ อาร์กิวเมนต์ที่กำหนดเอง และ Deep Link

activity<MyRoute> {
   label = getString(R.string.activity_title)
   // custom argument types, deepLinks...

   activityClass = MyActivity::class
}

คุณสามารถใช้ฟังก์ชัน navigation() ของ DSL เพื่อสร้างกราฟการนําทางที่ฝัง ฟังก์ชันนี้ใช้ประเภท สำหรับเส้นทางที่กำหนดให้กับกราฟนี้ และมีอาร์กิวเมนต์ 2 แบบ ดังนี้ เส้นทางไปยังจุดหมายเริ่มต้นของกราฟ และ lambda เพื่อไปให้ไกลขึ้น กำหนดค่ากราฟ องค์ประกอบที่ถูกต้อง ได้แก่ ปลายทางอื่นๆ ประเภทอาร์กิวเมนต์ที่กำหนดเอง Deep Link และป้ายกำกับที่สื่อความหมายสำหรับปลายทาง ป้ายกำกับนี้มีประโยชน์สำหรับการเชื่อมโยงกราฟการนำทางกับคอมโพเนนต์ UI โดยใช้ NavigationUI

@Serializable data object HomeGraph
@Serializable data object Home

navigation<HomeGraph>(startDestination = Home) {
   // label, other destinations, deep links
}

การรองรับปลายทางที่กำหนดเอง

หากคุณใช้ประเภทปลายทางใหม่ที่ไม่รองรับ Kotlin DSL โดยตรง คุณสามารถเพิ่มปลายทางเหล่านี้ลงใน Kotlin DSL ได้โดยใช้ addDestination() ดังนี้

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}
addDestination(customDestination)

หรืออีกวิธีหนึ่ง คุณสามารถใช้โอเปอเรเตอร์เครื่องหมายบวก (Unary Plus) เพื่อเพิ่ม ปลายทางที่สร้างขึ้นโดยตรงลงในกราฟ

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}

การระบุอาร์กิวเมนต์ปลายทาง

คุณกำหนดอาร์กิวเมนต์ปลายทางให้เป็นส่วนหนึ่งของคลาสเส้นทางได้ สิ่งเหล่านี้สามารถเป็นได้ ในลักษณะเดียวกับที่คุณทำสำหรับคลาส Kotlin ทั่วไป อาร์กิวเมนต์ที่ต้องระบุจะกำหนดเป็นประเภทที่ไม่ใช่ Null และอาร์กิวเมนต์ที่ไม่บังคับจะกำหนดด้วยค่าเริ่มต้น

กลไกพื้นฐานในการแสดงเส้นทางและอาร์กิวเมนต์ของเส้นทางจะอิงตามสตริง การใช้สตริงเพื่อจำลองเส้นทางช่วยให้จัดเก็บและกู้คืนสถานะการนําทางจากดิสก์ได้ในระหว่างการเปลี่ยนแปลงการกําหนดค่าและการสิ้นสุดกระบวนการที่ระบบเริ่ม ด้วยเหตุนี้ อาร์กิวเมนต์การนําทางแต่ละรายการจึงต้องจัดทําเป็นอนุกรมได้ กล่าวคือ ควรมีเมธอดที่แปลงการนําเสนอค่าอาร์กิวเมนต์ในหน่วยความจําเป็น String

การเรียงอันดับ Kotlin ปลั๊กอิน สร้างวิธีการทำให้ต่อเนื่องโดยอัตโนมัติสำหรับขั้นพื้นฐาน เมื่อ เพิ่มคำอธิบายประกอบ @Serializable ลงในออบเจ็กต์แล้ว

@Serializable
data class MyRoute(
  val id: String,
  val myList: List<Int>,
  val optionalArg: String? = null
)

fragment<MyFragment, MyRoute>

การระบุประเภทที่กำหนดเอง

สำหรับประเภทอาร์กิวเมนต์ที่กำหนดเอง คุณจะต้องระบุคลาส NavType ที่กำหนดเอง ซึ่งจะช่วยให้คุณควบคุมวิธีที่ระบบจะแยกวิเคราะห์ประเภทของคุณจากเส้นทางหรือ Deep Link ได้

ตัวอย่างเช่น เส้นทางที่ใช้กําหนดหน้าจอการค้นหาอาจมีคลาสที่แสดงพารามิเตอร์การค้นหา ดังนี้

@Serializable
data class SearchRoute(val parameters: SearchParameters)

@Serializable
@Parcelize
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

NavType ที่กําหนดเองอาจเขียนได้ดังนี้

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}

จากนั้นจึงนำไปใช้ใน Kotlin DSL ได้เช่นเดียวกับประเภทอื่นๆ

fragment<SearchFragment, SearchRoute>(
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) {
    label = getString(R.string.plant_search_title)
}

เมื่อนำทางไปยังจุดหมาย ให้สร้างอินสแตนซ์ของเส้นทางของคุณ

val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))

คุณดูพารามิเตอร์ได้จากเส้นทางในปลายทาง ดังนี้

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.parameters

Deep Link

คุณเพิ่ม Deep Link ไปยังปลายทางใดก็ได้ เช่นเดียวกับที่เพิ่ม Deep Link โดยใช้ XML กราฟการนำทาง ขั้นตอนเดียวกันทั้งหมดที่ระบุไว้ในหัวข้อการสร้าง Deep Link สําหรับปลายทางจะมีผลกับกระบวนการ ของการสร้าง Deep Link โดยใช้ Kotlin DSL

อย่างไรก็ตาม เมื่อสร้างDeep Link ที่ไม่ระบุ คุณจะไม่มีทรัพยากรการนำทาง XML ที่วิเคราะห์องค์ประกอบ <deepLink> ได้ คุณจึงไม่สามารถวาง<nav-graph> องค์ประกอบในไฟล์ AndroidManifest.xml และต้องเพิ่มตัวกรองIntent ลงในกิจกรรมด้วยตนเองแทน ความตั้งใจ ตัวกรองที่คุณระบุควรตรงกับเส้นทางพื้นฐาน การกระทำ และประเภท MIME ของ Deep Link ของแอปของคุณ

ระบบจะเพิ่ม Deep Link ไปยังปลายทางโดยการเรียกใช้ฟังก์ชัน deepLink ภายใน เรือแลมบ์ดาของปลายทาง โดยรับเส้นทางเป็นประเภทที่มีพารามิเตอร์ และพารามิเตอร์ basePath สำหรับเส้นทางฐานของ URL ที่ใช้สำหรับ Deep Link

คุณยังสามารถเพิ่มการดำเนินการและ mimetype โดยใช้ deepLinkBuilder ตามหลัง lambda

ตัวอย่างต่อไปนี้สร้าง URI ของ Deep Link สำหรับปลายทาง Home

@Serializable data object Home

fragment<HomeFragment, Home>{
  deepLink<Home>(basePath = "www.example.com/home"){
    // Optionally, specify the action and/or mime type that this destination
    // supports
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
  }
}

รูปแบบ URI

ระบบจะสร้างรูปแบบ URI ของ Deep Link โดยอัตโนมัติจากช่องของเส้นทางโดยใช้กฎต่อไปนี้

  • พารามิเตอร์ที่จําเป็นจะต่อท้ายเป็นพารามิเตอร์เส้นทาง (เช่น /{id})
  • พารามิเตอร์ที่มีค่าเริ่มต้น (พารามิเตอร์ที่ไม่บังคับ) จะเพิ่มเป็น query พารามิเตอร์ (เช่น ?name={name})
  • ระบบจะเพิ่มคอลเล็กชันต่อท้ายเป็นพารามิเตอร์การค้นหา (เช่น ?items={value1}&items={value2})
  • ลำดับของพารามิเตอร์ตรงกับลำดับของฟิลด์ในเส้นทาง

ตัวอย่างเช่น ประเภทเส้นทางต่อไปนี้

@Serializable data class PlantDetail(
  val id: String,
  val name: String,
  val colors: List<String>,
  val latinName: String? = null,
)

มีรูปแบบ URI ที่สร้างขึ้นดังนี้

basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}

คุณเพิ่ม Deep Link ได้ไม่จำกัดจำนวน ทุกครั้งที่คุณเรียกใช้ deepLink() ระบบจะเพิ่ม Deep Link ใหม่ต่อท้ายรายการที่ดูแลรักษาสำหรับปลายทางนั้น

ข้อจำกัด

ปลั๊กอิน Safe Args ใช้ร่วมกับ Kotlin DSL ไม่ได้ เนื่องจากปลั๊กอินจะค้นหาไฟล์ทรัพยากร XML เพื่อสร้างคลาส Directions และ Arguments