# 20 คำถามสัมภาษณ์ Jetpack Compose ยอดนิยมประจำปี 2026 > 20 คำถามสัมภาษณ์ Jetpack Compose ที่พบบ่อยที่สุด: recomposition, การจัดการ state, navigation, ประสิทธิภาพ และ pattern สถาปัตยกรรม พร้อมตัวอย่างโค้ดละเอียด - Published: 2026-01-27 - Updated: 2026-04-12 - Author: SharpSkill - Tags: jetpack compose, android, interview, kotlin, ui - Reading time: 14 min --- การสัมภาษณ์ 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 ```kotlin // compose_vs_xml.kt // 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 ```kotlin // recomposition_example.kt @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 ```kotlin // remember_vs_saveable.kt @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 ```kotlin // composable_lifecycle.kt @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 ทั้งหมด ```kotlin // state_hoisting.kt // 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 ที่ไม่จำเป็น ```kotlin // derived_state.kt @Composable fun ShoppingCart(items: List) { // 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 โดยตรง ทั้งสองเสริมกันในสถาปัตยกรรมสมัยใหม่ ```kotlin // stateflow_vs_compose_state.kt // ViewModel with StateFlow class UserViewModel : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()) val uiState: StateFlow = _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` ```kotlin // side_effects.kt @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 เปลี่ยนแปลง ```kotlin // coroutines_in_compose.kt @Composable fun SearchScreen(query: String) { var results by remember { mutableStateOf>(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 ได้อย่างเฉพาะเจาะจง ```kotlin // lazy_column.kt @Composable fun UserList(users: List) { 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 ในตำแหน่งที่ถูกต้อง) ```kotlin // custom_layout.kt @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 ตามวอลเปเปอร์ของผู้ใช้ ```kotlin // material_theme.kt // 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 เฉพาะ ```kotlin // navigation.kt @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 ```kotlin // passing_data.kt // 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("result", "") ?.collectAsState() // Use result } // Type-safe navigation (recommended) @Serializable data class DetailRoute(val itemId: String) // In NavHost composable { backStackEntry -> val route = backStackEntry.toRoute() DetailScreen(itemId = route.itemId) } ``` หลีกเลี่ยงการส่ง object ขนาดใหญ่ผ่าน argument การนำทาง ส่งเฉพาะ identifier แล้วโหลดข้อมูลเต็มใน ViewModel ของหน้าจอปลายทาง ## ประสิทธิภาพ ### 15. ป้องกัน recomposition ที่ไม่จำเป็นอย่างไร? Recomposition ที่มากเกินไปลดประสิทธิภาพ กลยุทธ์หลัก: ใช้ **stable types**, **lambda stabilization** และ **Compose compiler reports** เพื่อระบุปัญหา ```kotlin // preventing_recomposition.kt // 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) { 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 ```kotlin // profiling.kt // 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 ที่พบบ่อย ```kotlin // modifier_order.kt @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` เดียวที่แสดงถึง state ทั้งหมดของหน้าจอ ```kotlin // viewmodel_uistate.kt // Sealed interface for complete state representation sealed interface HomeUiState { data object Loading : HomeUiState data class Success( val articles: List
, val isRefreshing: Boolean = false ) : HomeUiState data class Error(val message: String) : HomeUiState } class HomeViewModel( private val repository: ArticleRepository ) : ViewModel() { val uiState: StateFlow = repository .getArticles() .map, 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 ไม่ใช่การทำงานภาพ ```kotlin // compose_testing.kt 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 ไปใช้ได้โดยไม่ต้องเขียนแอปทั้งหมดใหม่ ```kotlin // interop.kt // 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 สำหรับการย้ายทีละขั้น การฝึกปฏิบัติจริงกับโปรเจกต์จริงยังคงเป็นวิธีที่มีประสิทธิภาพที่สุดในการเชี่ยวชาญแนวคิดเหล่านี้ แต่ละคำถามที่กล่าวถึงในคู่มือนี้สมควรได้รับการสำรวจเชิงลึกพร้อมการนำโค้ดไปใช้จริง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/android/jetpack-compose-interview-questions