Jetpack Navigation Compose 2026: การนำทางแบบ Type-Safe และคำถามสัมภาษณ์งาน

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

Jetpack Navigation Compose 2026: การนำทางแบบ Type-Safe และคำถามสัมภาษณ์งาน

Jetpack Navigation Compose 2.10 นำเสนอความปลอดภัยของชนิดข้อมูลในขณะคอมไพล์สำหรับการนำทาง Android ผ่าน Kotlin Serialization แนวทางนี้ขจัดข้อผิดพลาด runtime จากการไม่ตรงกันของ argument และทำให้การ refactoring ง่ายขึ้นในโค้ดเบสขนาดใหญ่

Navigation 2.10 เวอร์ชันเสถียร

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:

build.gradle.ktskotlin
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:

Routes.ktkotlin
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 เฉพาะ:

AppNavigation.ktkotlin
@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 รวมพวกมันเข้าด้วยกัน:

AuthNavigation.ktkotlin
@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 เดียวกัน:

Domain.ktkotlin
@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 ทำการ map URI ภายนอกไปยัง route ที่ serializable กำหนด pattern ในบล็อก composable:

AppNavigation.ktkotlin
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:

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:

AppNavigation.ktkotlin
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 ขนาดใหญ่ได้อย่างไร?

หลีกเลี่ยง Argument ของ Route ที่มีขนาดใหญ่

Route ถูก serialize ลงใน saved instance state การส่ง object ขนาดใหญ่เช่นแคตตาล็อกสินค้าเสี่ยงต่อ TransactionTooLargeException ให้ส่ง ID แล้วโหลดข้อมูลจาก repository หรือ shared ViewModel แทน

รูปแบบสำหรับการแชร์ข้อมูลใช้ scoped ViewModel:

ProductViewModel.ktkotlin
@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 ที่ถูกต้อง:

NavigationTest.ktkotlin
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 และเปลี่ยนเส้นทางตามนั้น:

MainScreen.ktkotlin
@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 เดียวกัน:

AppNavigation.ktkotlin
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

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 1 กันยายน 2569

แชร์

บทความที่เกี่ยวข้อง