Jetpack Navigation Compose 2026: การนำทางแบบ Type-Safe และคำถามสัมภาษณ์งาน
เรียนรู้ Jetpack Navigation Compose 2.10 ที่มีการนำทางแบบ type-safe ด้วย Kotlin Serialization คู่มือฉบับสมบูรณ์พร้อมตัวอย่างโค้ดและคำถามสัมภาษณ์ Android

Jetpack Navigation Compose 2.10 นำเสนอความปลอดภัยของชนิดข้อมูลในขณะคอมไพล์สำหรับการนำทาง Android ผ่าน Kotlin Serialization แนวทางนี้ขจัดข้อผิดพลาด runtime จากการไม่ตรงกันของ argument และทำให้การ refactoring ง่ายขึ้นในโค้ดเบสขนาดใหญ่
Navigation Compose 2.10.0 ที่เปิดตัวในเดือนสิงหาคม 2026 ต้องการ SDK ขั้นต่ำ 24 Route แบบ type-safe ใช้ data class ที่มี @Serializable แทนที่ destination ที่เป็น string ทำให้ตรวจจับข้อผิดพลาดการนำทางได้ในขณะคอมไพล์
การตั้งค่าการนำทางแบบ Type-Safe ใน Navigation 2.10
API การนำทางแบบ type-safe ต้องการ Kotlin Serialization เพิ่ม dependency เหล่านี้ลงใน build.gradle.kts ระดับ module:
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
}
dependencies {
implementation("androidx.navigation:navigation-compose:2.10.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}กำหนด destination เป็นชนิด serializable ใช้ object สำหรับหน้าจอที่ไม่มี argument และ data class สำหรับหน้าจอที่รับ parameter:
import kotlinx.serialization.Serializable
@Serializable
object Home
@Serializable
object ProductList
@Serializable
data class ProductDetail(val productId: String)
@Serializable
data class Checkout(val cartId: String, val promoCode: String? = null)คอมไพเลอร์รับรองว่า class route ทั้งหมดถูกทำเครื่องหมายด้วย @Serializable การขาด annotation จะทำให้เกิดข้อผิดพลาด lint ใน Navigation 2.9+
การสร้าง NavHost ด้วย Route แบบ Serializable
Composable NavHost รับชนิด serializable โดยตรง แต่ละบล็อก composable<T> จัดการชนิด destination เฉพาะ:
@Composable
fun AppNavigation(
navController: NavHostController = rememberNavController()
) {
NavHost(
navController = navController,
startDestination = Home
) {
composable<Home> {
HomeScreen(
onNavigateToProducts = { navController.navigate(ProductList) }
)
}
composable<ProductList> {
ProductListScreen(
onProductClick = { productId ->
navController.navigate(ProductDetail(productId))
}
)
}
composable<ProductDetail> { backStackEntry ->
// ดึง route ที่มีชนิดจาก back stack entry
val route: ProductDetail = backStackEntry.toRoute()
ProductDetailScreen(
productId = route.productId,
onCheckout = { cartId ->
navController.navigate(Checkout(cartId))
}
)
}
composable<Checkout> { backStackEntry ->
val route: Checkout = backStackEntry.toRoute()
CheckoutScreen(
cartId = route.cartId,
promoCode = route.promoCode
)
}
}
}Extension toRoute<T>() คืนค่า instance ของ destination ที่มีชนิดครบถ้วน การเปลี่ยนชื่อ property ใน data class จะอัปเดตการใช้งานทั้งหมดโดยอัตโนมัติผ่านการ refactor ของ IDE
Nested Navigation Graph สำหรับ Feature Module
แอปพลิเคชันขนาดใหญ่ได้รับประโยชน์จาก nested navigation graph แต่ละ feature module กำหนด graph ของตัวเอง และ root graph รวมพวกมันเข้าด้วยกัน:
@Serializable
object AuthGraph // ตัวระบุ graph
@Serializable
object Login
@Serializable
object Register
@Serializable
data class PasswordReset(val email: String)
fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation<AuthGraph>(startDestination = Login) {
composable<Login> {
LoginScreen(
onRegisterClick = { navController.navigate(Register) },
onForgotPassword = { email ->
navController.navigate(PasswordReset(email))
},
onLoginSuccess = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<Register> {
RegisterScreen(
onRegistrationComplete = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<PasswordReset> { backStackEntry ->
val route: PasswordReset = backStackEntry.toRoute()
PasswordResetScreen(email = route.email)
}
}
}popUpTo<AuthGraph> ที่มี inclusive = true จะลบ flow การยืนยันตัวตนทั้งหมดออกจาก back stack หลังจากเข้าสู่ระบบสำเร็จ ป้องกันไม่ให้ผู้ใช้นำทางกลับไปที่หน้าจอเข้าสู่ระบบ
พร้อมที่จะพิชิตการสัมภาษณ์ Android แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Value Class เป็น Argument ของ Route
Navigation 2.9.0-alpha03 เพิ่มการสนับสนุน value class ใน route รูปแบบนี้บังคับความปลอดภัยของชนิดสำหรับ ID และป้องกันการสลับ parameter ที่มีชนิด primitive เดียวกัน:
@JvmInline
@Serializable
value class ProductId(val value: String)
@JvmInline
@Serializable
value class UserId(val value: String)
@Serializable
data class ProductDetail(val productId: ProductId)
@Serializable
data class UserProfile(val userId: UserId)ตอนนี้คอมไพเลอร์ป้องกันการส่ง UserId ไปยังที่ที่คาดหวัง ProductId ซึ่งเป็นบั๊กที่พบบ่อยในการนำทางที่ใช้ string
Deep Link กับ Route แบบ Type-Safe
Deep link ทำการ map URI ภายนอกไปยัง route ที่ serializable กำหนด pattern ในบล็อก composable:
composable<ProductDetail>(
deepLinks = listOf(
navDeepLink {
uriPattern = "https://example.com/products/{productId}"
}
)
) { backStackEntry ->
val route: ProductDetail = backStackEntry.toRoute()
ProductDetailScreen(productId = route.productId)
}Placeholder {productId} ถูก map โดยตรงกับ property productId ใน data class ProductDetail Navigation จะดึงและ deserialize argument โดยอัตโนมัติ
ลงทะเบียน deep link ใน AndroidManifest.xml:
<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/products" />
</intent-filter>
</activity>Predictive Back Gesture กับ Navigation Compose
Android 14+ รองรับ predictive back gesture ที่ผู้ใช้เห็นตัวอย่างหน้าจอก่อนหน้าก่อนที่จะดำเนินการ Navigation Compose 2.10 รวมเข้ากับฟีเจอร์นี้ผ่าน SeekableTransitionState:
NavHost(
navController = navController,
startDestination = Home,
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) },
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start) },
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End) },
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) }
) {
// destination
}Predictive back gesture แสดง destination ก่อนหน้าแบบ real-time ขณะที่ผู้ใช้ปัด การปล่อย gesture ก่อนถึง threshold จะยกเลิกการนำทางและกลับไปที่หน้าจอปัจจุบัน
คำถามสัมภาษณ์งานที่พบบ่อยเกี่ยวกับ Navigation Compose
การสัมภาษณ์ Android ระดับ senior มักจะเจาะลึกเรื่องการตัดสินใจด้านสถาปัตยกรรมการนำทาง ต่อไปนี้คือคำถามที่แยกแยะผู้สมัครที่มีประสบการณ์
ทำไมต้องใช้การนำทางแบบ type-safe แทน route ที่เป็น string?
Route ที่เป็น string เช่น "product/{productId}" ทำให้เกิด crash runtime เมื่อ argument ถูกสะกดผิดหรือมีชนิดไม่ถูกต้อง Route แบบ type-safe ตรวจจับข้อผิดพลาดเหล่านี้ในขณะคอมไพล์ การ refactor parameter ของ route จะอัปเดตการใช้งานทั้งหมดผ่าน IDE tooling ในขณะที่ route ที่เป็น string ต้องการการค้นหาและแทนที่ด้วยตนเองทั่วทั้งโค้ดเบส
จะแชร์ข้อมูลระหว่าง destination โดยไม่ส่ง object ขนาดใหญ่ได้อย่างไร?
Route ถูก serialize ลงใน saved instance state การส่ง object ขนาดใหญ่เช่นแคตตาล็อกสินค้าเสี่ยงต่อ TransactionTooLargeException ให้ส่ง ID แล้วโหลดข้อมูลจาก repository หรือ shared ViewModel แทน
รูปแบบสำหรับการแชร์ข้อมูลใช้ scoped ViewModel:
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
// ดึง route ที่มีชนิดจาก SavedStateHandle
private val productId: String = savedStateHandle.toRoute<ProductDetail>().productId
val product = repository.getProduct(productId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}จะทดสอบ navigation ใน Compose ได้อย่างไร?
การทดสอบ navigation ตรวจสอบว่าการกระทำของผู้ใช้ทำให้เกิดการเปลี่ยนแปลง destination ที่ถูกต้อง:
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun clickingProduct_navigatesToDetail() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
navController.navigatorProvider.addNavigator(ComposeNavigator())
composeTestRule.setContent {
AppNavigation(navController = navController)
}
// นำทางไปยังรายการสินค้า
composeTestRule.onNodeWithText("View Products").performClick()
// คลิกสินค้าแรก
composeTestRule.onNodeWithTag("product_item_0").performClick()
// ตรวจสอบ destination ปัจจุบัน
val currentRoute = navController.currentBackStackEntry?.toRoute<ProductDetail>()
assertThat(currentRoute).isNotNull()
}
}เกิดอะไรขึ้นเมื่อ NavController ถูกใช้หลังจากถูกทำลาย?
Navigation 2.9.0-alpha01 เปลี่ยนพฤติกรรมนี้ การใช้ NavController หลังจาก Activity หรือ Fragment ที่โฮสต์มันถูกทำลาย ตอนนี้จะโยน IllegalStateException เวอร์ชันก่อนหน้าล้มเหลวอย่างเงียบๆ หรือสร้างพฤติกรรมที่ไม่แน่นอน การเปลี่ยนแปลงนี้ทำให้พบบั๊ก lifecycle เร็วขึ้นในการพัฒนา
จะจัดการการนำทางแบบมีเงื่อนไขตามสถานะ auth ได้อย่างไร?
รูปแบบทั่วไปคือสังเกตสถานะ auth และเปลี่ยนเส้นทางตามนั้น:
@Composable
fun MainScreen(
authState: AuthState,
navController: NavHostController = rememberNavController()
) {
LaunchedEffect(authState) {
when (authState) {
is AuthState.Authenticated -> {
navController.navigate(Home) {
popUpTo(0) { inclusive = true }
}
}
is AuthState.Unauthenticated -> {
navController.navigate(AuthGraph) {
popUpTo(0) { inclusive = true }
}
}
else -> { /* กำลังโหลด ไม่ทำอะไร */ }
}
}
NavHost(
navController = navController,
startDestination = if (authState is AuthState.Authenticated) Home else AuthGraph
) {
authNavGraph(navController)
composable<Home> { HomeScreen() }
}
}LaunchedEffect ตอบสนองต่อการเปลี่ยนแปลงสถานะ auth และล้าง back stack ทั้งหมดด้วย popUpTo(0) { inclusive = true } เพื่อป้องกันการนำทางกลับไปยัง flow ที่ผิด
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
การย้ายจาก Route แบบ String ไปยังการนำทางแบบ Type-Safe
โปรเจกต์ที่มีอยู่สามารถย้ายได้ทีละขั้นตอน Route แบบ string และ route แบบ type-safe สามารถอยู่ร่วมกันใน NavHost เดียวกัน:
NavHost(
navController = navController,
startDestination = "home" // Route แบบ string เก่า
) {
// Destination แบบ string เก่า
composable("home") {
HomeScreen(onNavigateToProduct = { id ->
navController.navigate(ProductDetail(id)) // การนำทางแบบ type-safe ใหม่
})
}
// Destination แบบ type-safe ใหม่
composable<ProductDetail> { backStackEntry ->
val route: ProductDetail = backStackEntry.toRoute()
ProductDetailScreen(productId = route.productId)
}
}วิธีการนี้อนุญาตให้ย้ายทีละหน้าจอโดยไม่ต้องเขียน navigation graph ใหม่ทั้งหมด
ประเด็นสำคัญสำหรับ Jetpack Navigation Compose ในปี 2026
- Navigation 2.10.0 เป็นเวอร์ชันเสถียรปัจจุบันที่มี SDK ขั้นต่ำ 24
- Route แบบ type-safe ใช้ data class ที่มี
@Serializableแทน destination แบบ string - Value class ให้ความปลอดภัยของชนิดเพิ่มเติมสำหรับ ID และป้องกันการสลับ parameter
- Nested navigation graph จัดระเบียบ feature module ด้วย back stack อิสระ
- Deep link ทำการ map รูปแบบ URI โดยตรงกับ property ของ route
- Predictive back gesture รวมเข้ากันโดยอัตโนมัติกับ transition มาตรฐาน
- Argument ของ route ควรเบา: ส่ง ID แล้วโหลดข้อมูลจาก repository
- การย้ายจาก route แบบ string สามารถทำได้ทีละขั้นด้วย navigation graph แบบผสม
- การเตรียมตัวสัมภาษณ์ Android ครอบคลุมรูปแบบเหล่านี้อย่างละเอียด
- ดูเพิ่มเติมที่ คำถามสัมภาษณ์ Jetpack Compose สำหรับแนวคิด UI ที่เกี่ยวข้อง
คุณหาบั๊กใน Android เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 1 กันยายน 2569
แชร์
บทความที่เกี่ยวข้อง

การทำ Modularization บน Android ปี 2026: สถาปัตยกรรม Multi-Module และคำถามสัมภาษณ์
เชี่ยวชาญสถาปัตยกรรม multi-module ของ Android ด้วย convention plugins, Gradle version catalogs และ feature modules พร้อมคำถามสัมภาษณ์ที่พบบ่อยเกี่ยวกับกลยุทธ์ modularization

Android CameraX ปี 2026: คู่มือถ่ายภาพและวิดีโอพร้อมคำถามสัมภาษณ์
เรียนรู้ CameraX บน Android 2026: การถ่ายภาพ การบันทึกวิดีโอ การผสานรวม Jetpack Compose และคำถามสัมภาษณ์ทางเทคนิคสำหรับนักพัฒนา Android

Android WorkManager 2026: คู่มือฉบับสมบูรณ์ Background Tasks, Constraints และคำถามสัมภาษณ์งาน
เรียนรู้ Android WorkManager สำหรับการรัน background tasks พร้อม constraints, chaining และ CoroutineWorker รวมถึงคำถามสัมภาษณ์งานสำหรับ Android developer