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

การสัมภาษณ์ 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: 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
@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
@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
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 ทั้งหมด
// 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 ที่ไม่จำเป็น
@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 โดยตรง ทั้งสองเสริมกันในสถาปัตยกรรมสมัยใหม่
// 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
@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 เปลี่ยนแปลง
@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 ได้อย่างเฉพาะเจาะจง
@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 ในตำแหน่งที่ถูกต้อง)
@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 ตามวอลเปเปอร์ของผู้ใช้
// 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 เฉพาะ
@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
// 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 เพื่อระบุปัญหา
// 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
// 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 ที่พบบ่อย
@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 ทั้งหมดของหน้าจอ
// 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 ไม่ใช่การทำงานภาพ
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 ไปใช้ได้โดยไม่ต้องเขียนแอปทั้งหมดใหม่
// 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 สำหรับการย้ายทีละขั้น
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
การฝึกปฏิบัติจริงกับโปรเจกต์จริงยังคงเป็นวิธีที่มีประสิทธิภาพที่สุดในการเชี่ยวชาญแนวคิดเหล่านี้ แต่ละคำถามที่กล่าวถึงในคู่มือนี้สมควรได้รับการสำรวจเชิงลึกพร้อมการนำโค้ดไปใช้จริง
แท็ก
แชร์
บทความที่เกี่ยวข้อง

MVVM vs MVI บน Android: เลือก Architecture ไหนดีในปี 2026?
เปรียบเทียบ MVVM และ MVI บน Android อย่างละเอียด: ข้อดีข้อเสีย กรณีการใช้งาน และคู่มือปฏิบัติในการเลือก architecture ที่เหมาะสมในปี 2026

Kotlin Coroutines สำหรับ Android: คู่มือฉบับสมบูรณ์ 2026
คู่มือครบถ้วนเกี่ยวกับ Kotlin coroutines ในการพัฒนา Android: suspend functions, scopes, dispatchers และ patterns ขั้นสูง

25 คำถามสัมภาษณ์ Data Science ยอดนิยมในปี 2026
คำถามสัมภาษณ์ Data Science ที่ครอบคลุมสถิติ machine learning การเตรียมฟีเจอร์ deep learning SQL และการออกแบบระบบ พร้อมตัวอย่างโค้ด Python และคำตอบเชิงลึกสำหรับปี 2026