20 คำถามสัมภาษณ์ Jetpack Compose ยอดนิยมประจำปี 2026

20 คำถามสัมภาษณ์ Jetpack Compose ที่พบบ่อยที่สุด: recomposition, การจัดการ state, navigation, ประสิทธิภาพ และ pattern สถาปัตยกรรม พร้อมตัวอย่างโค้ดละเอียด

คำถามสัมภาษณ์ Jetpack Compose สำหรับนักพัฒนา Android

การสัมภาษณ์ Android สมัยใหม่ต้องการความเชี่ยวชาญ Jetpack Compose อย่างลึกซึ้ง Toolkit แบบ declarative นี้ได้กลายเป็นมาตรฐานของอุตสาหกรรม แทนที่แนวทาง XML แบบดั้งเดิม คู่มือนี้ครอบคลุม 20 คำถามที่พบบ่อยที่สุด ตั้งแต่พื้นฐานไปจนถึง pattern สถาปัตยกรรมขั้นสูง

เคล็ดลับการเตรียมตัว

ผู้สัมภาษณ์ไม่ได้ทดสอบเพียงความรู้ด้าน syntax เท่านั้น ความสามารถในการอธิบายกลไกภายในเช่น recomposition และ smart recomposition คือสิ่งที่แยกผู้สมัครที่ท่องจำออกจากผู้ที่เข้าใจ framework อย่างแท้จริง

พื้นฐาน Jetpack Compose

1. Jetpack Compose แตกต่างจาก XML แบบดั้งเดิมอย่างไร?

Jetpack Compose ใช้แนวทาง declarative: UI ถูกอธิบายเป็นฟังก์ชันของ state เมื่อ state เปลี่ยนแปลง framework จะคำนวณส่วน UI ที่ได้รับผลกระทบใหม่โดยอัตโนมัติ XML ใช้แนวทาง imperative: นักพัฒนาต้องจัดการ view ด้วยตนเองผ่าน findViewById และ setter

compose_vs_xml.ktkotlin
// Compose: declarative - UI is a function of state
@Composable
fun Greeting(name: String) {
    // UI automatically updates when name changes
    Text(text = "Hello, $name!")
}

// XML equivalent requires:
// 1. Layout XML file
// 2. findViewById in Activity
// 3. Manual setText call
// textView.text = "Hello, $name!"

ข้อดีของ Compose ได้แก่: กำจัด boilerplate, preview สดใน IDE, type safety เต็มรูปแบบด้วย Kotlin และความสามารถในการ compose ที่เป็นธรรมชาติมากกว่าการสืบทอดใน View

2. Recomposition คืออะไรและทำงานอย่างไร?

Recomposition คือกระบวนการที่ Compose เรียกฟังก์ชัน composable ใหม่เมื่อ state ที่อ่านเปลี่ยนแปลง Framework ดำเนินการ smart recomposition: เฉพาะ composable ที่อ่าน state ที่เปลี่ยนแปลงเท่านั้นที่จะถูกเรียกใหม่ ไม่ใช่ทั้ง tree

recomposition_example.ktkotlin
@Composable
fun Counter() {
    // When count changes, only this composable recomposes
    var count by remember { mutableStateOf(0) }

    Column {
        // This Text recomposes when count changes
        Text(text = "Count: $count")

        // This Button also recomposes (same scope)
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

// This composable does NOT recompose when count changes
@Composable
fun StaticHeader() {
    Text(text = "This never recomposes due to Counter")
}

Recomposition เป็นแบบ optimistic และสามารถถูกยกเลิกได้ Compose สามารถเริ่ม recomposition, ยกเลิกหาก state เปลี่ยนแปลงอีกครั้ง และเริ่มใหม่ด้วย state ล่าสุด

3. remember กับ rememberSaveable ต่างกันอย่างไร?

remember เก็บค่าในขณะที่ composable อยู่ใน composition ค่าจะหายไปเมื่อเกิด configuration change (หมุนหน้าจอ) rememberSaveable เก็บค่าผ่าน configuration change โดยใช้กลไก Bundle ของ Android

remember_vs_saveable.ktkotlin
@Composable
fun SearchBar() {
    // Lost on configuration change (rotation)
    var query by remember { mutableStateOf("") }

    // Survives configuration change
    var savedQuery by rememberSaveable { mutableStateOf("") }

    TextField(
        value = savedQuery,
        onValueChange = { savedQuery = it },
        placeholder = { Text("Search...") }
    )
}

ใช้ remember สำหรับ state ชั่วคราวที่ไม่จำเป็นต้องคงอยู่ (animation, state hover) ใช้ rememberSaveable สำหรับ input ของผู้ใช้และ state navigation ที่ต้องคงอยู่ผ่านการหมุนหน้าจอ

4. Compose จัดการวงจรชีวิตของ composable อย่างไร?

แต่ละ composable มีสามเฟส: เข้า composition (ถูกเรียกครั้งแรก), recomposition (ถูกเรียกใหม่เมื่อ state เปลี่ยน) และ ออกจาก composition (ถูกลบออกจาก tree) Lifecycle นี้แตกต่างจาก Activity/Fragment lifecycle

composable_lifecycle.ktkotlin
@Composable
fun UserProfile(userId: String) {
    // Enters composition: effect starts
    // Recomposition: effect restarts if userId changes
    // Leaves composition: effect is cancelled
    LaunchedEffect(userId) {
        // Coroutine tied to this composable's lifecycle
        val user = repository.getUser(userId)
    }

    // DisposableEffect for cleanup
    DisposableEffect(Unit) {
        val listener = database.addListener { /* ... */ }
        onDispose {
            // Called when leaving composition
            listener.remove()
        }
    }
}

การเข้าใจ lifecycle นี้มีความสำคัญอย่างยิ่งสำหรับการจัดการ side effect และหลีกเลี่ยง memory leak

การจัดการ State

5. State hoisting คืออะไรและทำไมถึงสำคัญ?

State hoisting คือ pattern การย้าย state ขึ้นไปยัง composable แม่ Composable ที่รับ state จะกลายเป็น stateless และง่ายต่อการทดสอบและนำกลับมาใช้ซ้ำ State ถูกยกขึ้นไปยังระดับต่ำสุดที่ครอบคลุม consumer ทั้งหมด

state_hoisting.ktkotlin
// Stateless: receives state, emits events
@Composable
fun EmailInput(
    email: String,
    onEmailChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    TextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        modifier = modifier
    )
}

// Stateful: owns and manages state
@Composable
fun LoginScreen() {
    var email by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }

    Column {
        EmailInput(
            email = email,
            onEmailChange = { email = it }
        )
        PasswordInput(
            password = password,
            onPasswordChange = { password = it }
        )
    }
}

Pattern นี้ปฏิบัติตามหลักการ unidirectional data flow: state ไหลลงล่าง event ไหลขึ้นบน

6. เมื่อใดควรใช้ derivedStateOf?

derivedStateOf สร้าง state ที่คำนวณจาก state อื่น และจะกระตุ้น recomposition เฉพาะเมื่อผลลัพธ์การคำนวณเปลี่ยนแปลง มีประโยชน์ในการหลีกเลี่ยง recomposition ที่ไม่จำเป็น

derived_state.ktkotlin
@Composable
fun ShoppingCart(items: List<CartItem>) {
    // Without derivedStateOf: recomposes on EVERY list change
    // val total = items.sumOf { it.price }

    // With derivedStateOf: recomposes only when total changes
    val total by remember(items) {
        derivedStateOf { items.sumOf { it.price } }
    }

    // Only recomposes when hasDiscount value changes
    val hasDiscount by remember(total) {
        derivedStateOf { total > 100.0 }
    }

    Text("Total: $$total")
    if (hasDiscount) {
        Text("Discount applied!")
    }
}

ใช้ derivedStateOf เมื่อ state ต้นทางเปลี่ยนบ่อยแต่ผลลัพธ์ที่ได้มาไม่ค่อยเปลี่ยน อย่าใช้สำหรับการแปลงง่าย ๆ ที่ให้ค่าต่างกันเสมอ

7. StateFlow แตกต่างจาก Compose State อย่างไร?

StateFlow มาจาก Kotlin coroutines และไม่ขึ้นกับ platform MutableState ของ Compose รวมเข้ากับระบบ recomposition โดยตรง ทั้งสองเสริมกันในสถาปัตยกรรมสมัยใหม่

stateflow_vs_compose_state.ktkotlin
// ViewModel with StateFlow
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(name = name) }
    }
}

data class UserUiState(
    val name: String = "",
    val isLoading: Boolean = false
)

// Composable collecting StateFlow
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    // collectAsState bridges StateFlow to Compose State
    val uiState by viewModel.uiState.collectAsState()

    if (uiState.isLoading) {
        CircularProgressIndicator()
    } else {
        Text(uiState.name)
    }
}

ใช้ StateFlow ใน ViewModel สำหรับ logic ธุรกิจ ใช้ MutableState สำหรับ state UI ภายใน composable

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

Side Effect

8. Side Effect API หลักใน Compose มีอะไรบ้าง?

Compose มี API หลายตัวสำหรับดำเนินการที่มีผลข้างเคียงนอก scope ของ composable: LaunchedEffect, DisposableEffect, SideEffect และ rememberCoroutineScope

side_effects.ktkotlin
@Composable
fun AnalyticsScreen(screenName: String) {
    // LaunchedEffect: runs suspend function, restarts on key change
    LaunchedEffect(screenName) {
        analytics.logScreenView(screenName)
    }

    // DisposableEffect: setup + cleanup
    DisposableEffect(Unit) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) {
                analytics.logResume()
            }
        }
        lifecycle.addObserver(observer)
        onDispose {
            lifecycle.removeObserver(observer)
        }
    }

    // SideEffect: runs on every successful recomposition
    SideEffect {
        // Update non-Compose state
        analyticsHelper.setCurrentScreen(screenName)
    }
}

@Composable
fun ActionButton() {
    // rememberCoroutineScope: for event-driven coroutines
    val scope = rememberCoroutineScope()

    Button(onClick = {
        scope.launch {
            // Cannot use LaunchedEffect here (not composable context)
            performAction()
        }
    }) {
        Text("Execute")
    }
}

กฎคือ: LaunchedEffect สำหรับการดำเนินการที่ผูกกับ lifecycle ของ composable, rememberCoroutineScope สำหรับการดำเนินการที่ถูกกระตุ้นโดยเหตุการณ์ของผู้ใช้

9. จัดการ coroutine ภายใน Compose อย่างไร?

Coroutine ใน Compose ถูกจัดการผ่าน scope ที่ผูกกับ lifecycle ของ composable LaunchedEffect ยกเลิก coroutine เมื่อ composable ออกจาก composition หรือ key เปลี่ยนแปลง

coroutines_in_compose.ktkotlin
@Composable
fun SearchScreen(query: String) {
    var results by remember { mutableStateOf<List<Result>>(emptyList()) }

    // Debounced search: cancels previous on new query
    LaunchedEffect(query) {
        delay(300) // debounce
        results = searchRepository.search(query)
    }
}

@Composable
fun InfiniteAnimation() {
    val infiniteTransition = rememberInfiniteTransition(label = "pulse")
    val alpha by infiniteTransition.animateFloat(
        initialValue = 0.3f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse
        ),
        label = "alpha"
    )

    Box(
        modifier = Modifier
            .size(100.dp)
            .alpha(alpha)
            .background(Color.Blue)
    )
}

ห้ามเปิด coroutine ใน body ของ composable โดยไม่ใช้ side effect API เพราะจะทำให้เปิด coroutine ใหม่ทุกครั้งที่เกิด recomposition

Layout

10. LazyColumn ทำงานอย่างไรและเพิ่มประสิทธิภาพอย่างไร?

LazyColumn จะ compose และ layout เฉพาะ item ที่แสดงบนหน้าจอ คล้ายกับ RecyclerView พารามิเตอร์ key มีความสำคัญต่อประสิทธิภาพเพราะช่วยให้ Compose ระบุตัวตน item ได้อย่างเฉพาะเจาะจง

lazy_column.ktkotlin
@Composable
fun UserList(users: List<User>) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(
            items = users,
            // Key helps Compose track items across changes
            key = { user -> user.id }
        ) { user ->
            UserCard(user = user)
        }
    }
}

// Optimized with remember and stable types
@Composable
fun UserCard(user: User) {
    // Skipped during recomposition if user hasn't changed
    Card {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = user.name, style = MaterialTheme.typography.titleMedium)
            Text(text = user.email, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

หลีกเลี่ยงการดำเนินการหนักภายใน lambda items ใช้ remember สำหรับการคำนวณที่มีต้นทุนสูง และใช้ data class เพื่อให้ Compose ตรวจสอบ equality ได้ถูกต้อง

11. สร้าง custom Layout ใน Compose อย่างไร?

Custom Layout ให้การควบคุมเต็มรูปแบบในการวัดและจัดวาง child กระบวนการนี้ประกอบด้วยสองเฟส: measure (วัดแต่ละ child) และ place (วาง child ในตำแหน่งที่ถูกต้อง)

custom_layout.ktkotlin
@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    columns: Int = 2,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val columnWidth = constraints.maxWidth / columns
        val itemConstraints = constraints.copy(
            minWidth = columnWidth,
            maxWidth = columnWidth
        )

        // Measure phase
        val placeables = measurables.map { it.measure(itemConstraints) }

        // Track height per column
        val columnHeights = IntArray(columns)
        val positions = placeables.map { placeable ->
            val col = columnHeights.indexOfMin()
            val position = IntOffset(col * columnWidth, columnHeights[col])
            columnHeights[col] += placeable.height
            position
        }

        val height = columnHeights.max()

        // Place phase
        layout(constraints.maxWidth, height) {
            placeables.forEachIndexed { index, placeable ->
                placeable.placeRelative(positions[index])
            }
        }
    }
}

Custom Layout ควรใช้เมื่อ Row, Column และ Box ไม่เพียงพอต่อความต้องการ สำหรับกรณีที่ง่ายกว่า Modifier.layout เพียงพอแล้ว

12. ใช้ MaterialTheme ใน Compose อย่างไร?

MaterialTheme จัดเตรียมระบบออกแบบที่สอดคล้องผ่านสามเสาหลัก: color scheme, typography และ shapes Material 3 (Material You) รองรับ dynamic color ตามวอลเปเปอร์ของผู้ใช้

material_theme.ktkotlin
// Custom theme definition
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) {
        darkColorScheme(
            primary = Color(0xFFBB86FC),
            secondary = Color(0xFF03DAC6),
            background = Color(0xFF121212)
        )
    } else {
        lightColorScheme(
            primary = Color(0xFF6200EE),
            secondary = Color(0xFF03DAC6),
            background = Color(0xFFFFFFFF)
        )
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}

// Usage in composables
@Composable
fun ThemedButton() {
    Button(
        onClick = { },
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.primary
        )
    ) {
        Text(
            text = "Themed",
            style = MaterialTheme.typography.labelLarge
        )
    }
}

ใช้ token จาก MaterialTheme เสมอแทนการ hardcode สีหรือขนาดฟอนต์ เพื่อรับประกันความสอดคล้องและง่ายต่อการใช้งาน dark mode

การนำทาง

13. สร้างระบบนำทางด้วย NavHost อย่างไร?

NavHost จาก library Navigation Compose จัดการ back stack และการเปลี่ยนผ่านระหว่างหน้าจอ แต่ละจุดหมายถูกกำหนดเป็น composable พร้อม route เฉพาะ

navigation.ktkotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { id ->
                    navController.navigate("detail/$id")
                }
            )
        }

        composable(
            route = "detail/{itemId}",
            arguments = listOf(
                navArgument("itemId") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")
            DetailScreen(itemId = itemId)
        }

        composable("settings") {
            SettingsScreen(
                onBack = { navController.popBackStack() }
            )
        }
    }
}

ห้ามส่ง navController ตรง ๆ ไปยัง composable ลูก ใช้ callback lambda เพื่อให้ composable สามารถทดสอบได้และไม่ผูกติดกับการนำทาง

14. ส่งข้อมูลระหว่างหน้าจอใน Compose อย่างไร?

ข้อมูลง่าย ๆ ส่งผ่าน argument ของ route สำหรับ object ที่ซับซ้อน ใช้ savedStateHandle จาก ViewModel หรือ shared ViewModel ที่มี scope ของ navigation graph

passing_data.ktkotlin
// Simple arguments via route
navController.navigate("detail/${item.id}")

// Complex data via savedStateHandle
class DetailViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    val itemId: String = checkNotNull(savedStateHandle["itemId"])

    // Or pass result back to previous screen
    fun setResult(result: String) {
        savedStateHandle["result"] = result
    }
}

// Reading result in previous screen
@Composable
fun ListScreen(navController: NavController) {
    val result = navController.currentBackStackEntry
        ?.savedStateHandle
        ?.getStateFlow<String>("result", "")
        ?.collectAsState()

    // Use result
}

// Type-safe navigation (recommended)
@Serializable
data class DetailRoute(val itemId: String)

// In NavHost
composable<DetailRoute> { backStackEntry ->
    val route = backStackEntry.toRoute<DetailRoute>()
    DetailScreen(itemId = route.itemId)
}

หลีกเลี่ยงการส่ง object ขนาดใหญ่ผ่าน argument การนำทาง ส่งเฉพาะ identifier แล้วโหลดข้อมูลเต็มใน ViewModel ของหน้าจอปลายทาง

ประสิทธิภาพ

15. ป้องกัน recomposition ที่ไม่จำเป็นอย่างไร?

Recomposition ที่มากเกินไปลดประสิทธิภาพ กลยุทธ์หลัก: ใช้ stable types, lambda stabilization และ Compose compiler reports เพื่อระบุปัญหา

preventing_recomposition.ktkotlin
// Stable data class - Compose can skip recomposition
@Immutable
data class UserData(
    val id: String,
    val name: String,
    val avatarUrl: String
)

// Lambda stabilization with remember
@Composable
fun ParentScreen(viewModel: MyViewModel = viewModel()) {
    // Without remember: new lambda instance on every recomposition
    // UserList(onItemClick = { id -> viewModel.selectItem(id) })

    // With remember: stable lambda reference
    val onItemClick = remember<(String) -> Unit> {
        { id -> viewModel.selectItem(id) }
    }

    UserList(onItemClick = onItemClick)
}

// Using key to control recomposition scope
@Composable
fun ItemList(items: List<Item>) {
    LazyColumn {
        items(items, key = { it.id }) { item ->
            // Each item recomposes independently
            key(item.id) {
                ItemRow(item = item)
            }
        }
    }
}

เปิดใช้ Compose compiler metrics (-P plugin:...metricsDestination) เพื่อระบุ composable ที่ไม่เสถียร

16. ทำ profiling แอป Compose อย่างไร?

Android Studio มี Layout Inspector ที่แสดงจำนวน recomposition แบบ real-time Compose ยังมี modifier พิเศษสำหรับการ debug

profiling.ktkotlin
// Recomposition counter (debug only)
@Composable
fun DebugRecomposition(tag: String) {
    if (BuildConfig.DEBUG) {
        val recompositionCount = remember { mutableIntStateOf(0) }
        SideEffect {
            recompositionCount.intValue++
        }
        Log.d("Recomposition", "$tag: ${recompositionCount.intValue}")
    }
}

// Performance tracing
@Composable
fun TracedScreen() {
    trace("TracedScreen") {
        // Content here is measured in system trace
        HeavyContent()
    }
}

// Baseline profiles for startup optimization
@ExperimentalBaselineProfilesApi
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generateProfile() {
        rule.collect("com.example.app") {
            startActivityAndWait()
            // Navigate through critical user flows
        }
    }
}

ใช้ Layout Inspector ใน Android Studio เพื่อติดตามจำนวน recomposition Composable ที่มีจำนวนสูงโดยไม่มีการเปลี่ยนแปลงภาพบ่งบอกถึงปัญหาประสิทธิภาพ

17. ทำไมลำดับ Modifier ถึงสำคัญ?

Modifier ใน Compose ถูกใช้งาน ตามลำดับจากนอกเข้าใน ลำดับที่ต่างกันให้ผลลัพธ์ภาพที่ต่างกัน ข้อผิดพลาดเรื่องลำดับ Modifier เป็นแหล่งที่มาของ bug UI ที่พบบ่อย

modifier_order.ktkotlin
@Composable
fun ModifierOrderDemo() {
    // Order 1: padding THEN background
    // Background covers padded area
    Box(
        modifier = Modifier
            .padding(16.dp)
            .background(Color.Red)
            .size(100.dp)
    )

    // Order 2: background THEN padding
    // Background extends to full size, padding is inside
    Box(
        modifier = Modifier
            .background(Color.Blue)
            .padding(16.dp)
            .size(100.dp)
    )

    // Clickable area depends on order
    Box(
        modifier = Modifier
            .padding(16.dp)        // Padding outside clickable
            .clickable { }          // Click area
            .padding(8.dp)          // Padding inside clickable
            .background(Color.Green)
    )
}

กฎปฏิบัติ: อ่าน chain ของ Modifier จากบนลงล่างเสมือนชั้นจากนอกเข้าใน padding ก่อน background เพิ่มพื้นที่ว่างนอก background ส่วนหลังจากนั้นเพิ่มพื้นที่ว่างด้านใน

ข้อผิดพลาดที่พบบ่อย

การวาง clickable หลัง padding ทำให้พื้นที่ padding ไม่สามารถคลิกได้ เพื่อให้พื้นที่สัมผัสใหญ่ขึ้น ให้วาง clickable ก่อน padding

สถาปัตยกรรม

18. Pattern ViewModel + UiState ทำงานกับ Compose อย่างไร?

Pattern นี้แยก logic ธุรกิจ (ViewModel) ออกจากการนำเสนอ (Composable) ViewModel เปิดเผย StateFlow<UiState> เดียวที่แสดงถึง state ทั้งหมดของหน้าจอ

viewmodel_uistate.ktkotlin
// Sealed interface for complete state representation
sealed interface HomeUiState {
    data object Loading : HomeUiState
    data class Success(
        val articles: List<Article>,
        val isRefreshing: Boolean = false
    ) : HomeUiState
    data class Error(val message: String) : HomeUiState
}

class HomeViewModel(
    private val repository: ArticleRepository
) : ViewModel() {
    val uiState: StateFlow<HomeUiState> = repository
        .getArticles()
        .map<List<Article>, HomeUiState> { HomeUiState.Success(it) }
        .catch { emit(HomeUiState.Error(it.message ?: "Unknown error")) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = HomeUiState.Loading
        )

    fun refresh() {
        viewModelScope.launch {
            // Update state to show refreshing
            repository.refresh()
        }
    }
}

// Composable consumes UiState
@Composable
fun HomeScreen(viewModel: HomeViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    when (val state = uiState) {
        is HomeUiState.Loading -> LoadingIndicator()
        is HomeUiState.Success -> ArticleList(
            articles = state.articles,
            onRefresh = viewModel::refresh
        )
        is HomeUiState.Error -> ErrorMessage(state.message)
    }
}

ใช้ sealed interface สำหรับ UiState เพื่อให้ Compose ดำเนินการ exhaustive when check ได้ WhileSubscribed(5000) หยุด collection เมื่อไม่มี subscriber ช่วยประหยัดทรัพยากร

19. ทดสอบ composable อย่างไร?

Compose มี ComposeTestRule สำหรับการทดสอบ UI Test ถูกเขียนโดยใช้ semantic tree ไม่ใช่การทำงานภาพ

compose_testing.ktkotlin
class LoginScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun loginButton_disabledWhenFieldsEmpty() {
        composeTestRule.setContent {
            LoginScreen()
        }

        // Find by test tag
        composeTestRule
            .onNodeWithTag("loginButton")
            .assertIsNotEnabled()
    }

    @Test
    fun loginButton_enabledWhenFieldsFilled() {
        composeTestRule.setContent {
            LoginScreen()
        }

        // Type in fields
        composeTestRule
            .onNodeWithTag("emailField")
            .performTextInput("test@example.com")

        composeTestRule
            .onNodeWithTag("passwordField")
            .performTextInput("password123")

        // Verify button is enabled
        composeTestRule
            .onNodeWithTag("loginButton")
            .assertIsEnabled()
    }

    @Test
    fun errorMessage_displayedOnFailure() {
        composeTestRule.setContent {
            LoginScreen()
        }

        // Verify error message appears
        composeTestRule
            .onNodeWithText("Invalid credentials")
            .assertExists()
    }
}

ใช้ testTag ใน Modifier เพื่อค้นหา node ได้ง่าย หลีกเลี่ยงการทดสอบตามตำแหน่งภาพเพราะเปราะบางต่อการเปลี่ยนแปลง layout

20. รวม Compose กับ View XML ที่มีอยู่อย่างไร?

การย้ายทีละขั้นจาก XML ไปยัง Compose ทำได้ผ่าน ComposeView (Compose ภายใน XML) และ AndroidView (XML ภายใน Compose) กลยุทธ์นี้ช่วยให้นำ Compose ไปใช้ได้โดยไม่ต้องเขียนแอปทั้งหมดใหม่

interop.ktkotlin
// Compose inside XML layout
class ExistingFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
            )
            setContent {
                AppTheme {
                    NewComposeFeature()
                }
            }
        }
    }
}

// XML View inside Compose
@Composable
fun LegacyMapView() {
    AndroidView(
        factory = { context ->
            // Create traditional Android View
            MapView(context).apply {
                // Configure the view
                onCreate(Bundle())
            }
        },
        update = { mapView ->
            // Update view when state changes
            mapView.getMapAsync { map ->
                map.moveCamera(CameraUpdateFactory.newLatLng(location))
            }
        },
        onRelease = { mapView ->
            // Cleanup when leaving composition
            mapView.onDestroy()
        }
    )
}

กลยุทธ์การย้ายที่แนะนำ: เริ่มจากหน้าจอใหม่ด้วย Compose จากนั้นย้ายหน้าจอเดิมทีละขั้นจาก leaf composable ขึ้นไป

สรุป

20 คำถามนี้ครอบคลุมแง่มุมสำคัญของการสัมภาษณ์ Jetpack Compose: จากพื้นฐาน declarative ไปจนถึง pattern สถาปัตยกรรมที่พร้อมใช้งานจริง กุญแจสู่ความสำเร็จอยู่ที่ความเข้าใจอย่างลึกซึ้งเกี่ยวกับกลไก recomposition การจัดการ state อย่างถูกต้อง และประสบการณ์จริงในการสร้างแอปพลิเคชัน

Checklist การเตรียมตัว

  • เข้าใจความแตกต่างระหว่าง Compose แบบ declarative กับ XML แบบ imperative
  • เข้าใจกลไก recomposition และ smart recomposition
  • ฝึก state hoisting และ unidirectional data flow
  • ใช้ side effect อย่างถูกต้อง (LaunchedEffect, DisposableEffect)
  • เพิ่มประสิทธิภาพ LazyColumn ด้วย key และ stable types
  • เข้าใจ pattern ViewModel + UiState ด้วย sealed interface
  • เขียน compose test โดยใช้ semantic tree
  • เข้าใจ interop Compose-XML สำหรับการย้ายทีละขั้น

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

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

แท็ก

#jetpack compose
#android
#interview
#kotlin
#ui

แชร์

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