# 20 Câu Hỏi Phỏng Vấn Jetpack Compose Hàng Đầu Năm 2026 > 20 câu hỏi phỏng vấn Jetpack Compose thường gặp nhất: recomposition, quản lý state, navigation, hiệu năng và các pattern kiến trúc với ví dụ code chi tiết. - Published: 2026-01-27 - Updated: 2026-04-12 - Author: SharpSkill - Tags: jetpack compose, android, interview, kotlin, ui - Reading time: 14 min --- Phỏng vấn Android hiện đại đòi hỏi sự thành thạo Jetpack Compose. Toolkit khai báo này đã trở thành tiêu chuẩn ngành, thay thế cách tiếp cận XML truyền thống. Hướng dẫn này bao gồm 20 câu hỏi thường gặp nhất, từ kiến thức cơ bản đến các pattern kiến trúc nâng cao. > **Mẹo Chuẩn Bị** > > Người phỏng vấn không chỉ kiểm tra kiến thức cú pháp. Khả năng giải thích các cơ chế nội bộ như recomposition và smart recomposition là yếu tố phân biệt giữa ứng viên học thuộc và người thực sự hiểu framework. ## Kiến Thức Cơ Bản Jetpack Compose ### 1. Sự khác biệt giữa Jetpack Compose và XML truyền thống là gì? Jetpack Compose sử dụng cách tiếp cận **khai báo**: UI được mô tả như một hàm của state. Khi state thay đổi, framework tự động tính toán lại phần UI bị ảnh hưởng. XML sử dụng cách tiếp cận **mệnh lệnh**: lập trình viên phải thủ công thao tác view thông qua `findViewById` và 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!" ``` Ưu điểm của Compose bao gồm: loại bỏ boilerplate, xem trước trực tiếp trong IDE, type safety hoàn toàn với Kotlin và khả năng composability tự nhiên hơn so với kế thừa trong View. ### 2. Recomposition là gì và hoạt động như thế nào? **Recomposition** là quá trình Compose gọi lại hàm composable khi state mà nó đọc thay đổi. Framework thực hiện **smart recomposition**: chỉ những composable đọc state thay đổi mới được gọi lại, không phải toàn bộ 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 mang tính **lạc quan** và có thể bị hủy bỏ. Compose có thể bắt đầu recomposition, hủy bỏ nếu state thay đổi lần nữa, và bắt đầu lại với state mới nhất. ### 3. Phân biệt remember và rememberSaveable? `remember` lưu trữ giá trị trong khi composable còn nằm trong composition. Giá trị mất khi configuration change (xoay màn hình). `rememberSaveable` lưu trữ giá trị qua configuration change sử dụng cơ chế `Bundle` của 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...") } ) } ``` Sử dụng `remember` cho state tạm thời không cần lưu giữ (animation, state hover). Sử dụng `rememberSaveable` cho input người dùng và state điều hướng cần tồn tại qua việc xoay màn hình. ### 4. Compose quản lý vòng đời composable như thế nào? Mỗi composable có ba giai đoạn: **vào composition** (lần đầu được gọi), **recomposition** (được gọi lại khi state thay đổi) và **rời composition** (bị xóa khỏi tree). Lifecycle này khác với 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() } } } ``` Hiểu vòng đời này rất quan trọng để quản lý side effect và tránh memory leak. ## Quản Lý State ### 5. State hoisting là gì và tại sao quan trọng? **State hoisting** là pattern di chuyển state lên composable cha. Composable nhận state trở thành **stateless** và dễ kiểm thử cũng như tái sử dụng hơn. State được nâng lên cấp thấp nhất bao gồm tất cả 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 này tuân theo nguyên tắc **unidirectional data flow**: state chảy xuống dưới, event chảy lên trên. ### 6. Khi nào sử dụng derivedStateOf? `derivedStateOf` tạo state được tính toán từ state khác và chỉ kích hoạt recomposition khi kết quả tính toán thay đổi. Hữu ích để tránh recomposition không cần thiết. ```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!") } } ``` Sử dụng `derivedStateOf` khi state nguồn thay đổi thường xuyên nhưng kết quả đạo hàm ít thay đổi. Không sử dụng cho phép biến đổi đơn giản luôn tạo ra giá trị khác. ### 7. StateFlow khác gì với Compose State? `StateFlow` từ Kotlin coroutines và không phụ thuộc platform. `MutableState` từ Compose tích hợp trực tiếp với hệ thống recomposition. Cả hai bổ sung cho nhau trong kiến trúc hiện đại. ```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) } } ``` Sử dụng `StateFlow` trong ViewModel cho logic nghiệp vụ. Sử dụng `MutableState` cho state UI cục bộ trong composable. ## Side Effect ### 8. Các Side Effect API chính trong Compose là gì? Compose cung cấp nhiều API để thực thi các thao tác có hiệu ứng phụ ngoài scope composable: `LaunchedEffect`, `DisposableEffect`, `SideEffect` và `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") } } ``` Quy tắc: `LaunchedEffect` cho các thao tác gắn với lifecycle composable, `rememberCoroutineScope` cho các thao tác được kích hoạt bởi sự kiện người dùng. ### 9. Quản lý coroutine bên trong Compose như thế nào? Coroutine trong Compose được quản lý thông qua scope gắn với lifecycle composable. `LaunchedEffect` hủy coroutine khi composable rời khỏi composition hoặc key thay đổi. ```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) ) } ``` Không bao giờ khởi chạy coroutine trong body composable mà không có side effect API. Điều này gây ra việc khởi chạy coroutine mới mỗi lần recomposition. ## Layout ### 10. LazyColumn hoạt động như thế nào và tối ưu ra sao? `LazyColumn` chỉ compose và layout các item hiển thị trên màn hình, tương tự `RecyclerView`. Tham số `key` rất quan trọng cho hiệu năng vì giúp Compose nhận diện từng item một cách duy nhất. ```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) } } } ``` Tránh các thao tác nặng trong lambda `items`. Sử dụng `remember` cho các phép tính tốn kém và đảm bảo sử dụng data class để Compose có thể thực hiện equality check chính xác. ### 11. Tạo custom Layout trong Compose như thế nào? Custom `Layout` cho phép kiểm soát hoàn toàn việc đo lường và đặt vị trí child. Quá trình này gồm hai giai đoạn: **measure** (đo từng child) và **place** (đặt child vào vị trí chính xác). ```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 nên được sử dụng khi Row, Column và Box không đủ đáp ứng nhu cầu. Cho các trường hợp đơn giản hơn, `Modifier.layout` là đủ. ### 12. Sử dụng MaterialTheme trong Compose như thế nào? `MaterialTheme` cung cấp hệ thống thiết kế nhất quán thông qua ba trụ cột: **color scheme**, **typography** và **shapes**. Material 3 (Material You) hỗ trợ dynamic color dựa trên hình nền người dùng. ```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 ) } } ``` Luôn sử dụng token từ `MaterialTheme` thay vì hardcode màu sắc hoặc kích thước font. Điều này đảm bảo tính nhất quán và dễ dàng triển khai dark mode. ## Điều Hướng ### 13. Triển khai điều hướng với NavHost như thế nào? `NavHost` từ thư viện Navigation Compose quản lý back stack và chuyển đổi giữa các màn hình. Mỗi điểm đến được định nghĩa là composable với route duy nhất. ```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() } ) } } } ``` Không bao giờ truyền `navController` trực tiếp cho composable con. Sử dụng callback lambda để composable có thể kiểm thử được và không phụ thuộc vào điều hướng. ### 14. Gửi dữ liệu giữa các màn hình trong Compose như thế nào? Dữ liệu đơn giản được gửi qua tham số route. Cho đối tượng phức tạp, sử dụng `savedStateHandle` từ ViewModel hoặc shared ViewModel với 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) } ``` Tránh gửi đối tượng lớn qua tham số điều hướng. Chỉ gửi identifier, sau đó tải dữ liệu đầy đủ trong ViewModel màn hình đích. ## Hiệu Năng ### 15. Ngăn chặn recomposition không cần thiết như thế nào? Recomposition quá mức làm giảm hiệu năng. Chiến lược chính: sử dụng **stable types**, **lambda stabilization** và **Compose compiler reports** để xác định vấn đề. ```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) } } } } ``` Kích hoạt Compose compiler metrics (`-P plugin:...metricsDestination`) để xác định các composable không ổn định. ### 16. Profiling ứng dụng Compose như thế nào? Android Studio cung cấp **Layout Inspector** hiển thị số lần recomposition theo thời gian thực. Compose cũng cung cấp modifier đặc biệt cho việc 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 } } } ``` Sử dụng Layout Inspector trong Android Studio để theo dõi số lần recomposition. Composable có số lần cao mà không có thay đổi trực quan cho thấy vấn đề hiệu năng. ### 17. Tại sao thứ tự Modifier quan trọng? Modifier trong Compose được áp dụng **theo thứ tự từ ngoài vào trong**. Thứ tự khác nhau tạo ra kết quả trực quan khác nhau. Lỗi thứ tự Modifier là nguồn bug UI phổ biến. ```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) ) } ``` Quy tắc thực tế: đọc chuỗi Modifier từ trên xuống dưới như các lớp từ ngoài vào trong. `padding` trước `background` thêm khoảng cách ngoài background, sau đó thêm khoảng cách bên trong. > **Lỗi Thường Gặp** > > Đặt `clickable` sau `padding` làm cho vùng padding không thể nhấn được. Để có vùng chạm lớn hơn, đặt `clickable` trước `padding`. ## Kiến Trúc ### 18. Pattern ViewModel + UiState hoạt động như thế nào với Compose? Pattern này tách biệt logic nghiệp vụ (ViewModel) khỏi phần trình bày (Composable). ViewModel cung cấp một `StateFlow` duy nhất đại diện cho toàn bộ state màn hình. ```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) } } ``` Sử dụng `sealed interface` cho UiState để Compose có thể thực hiện exhaustive `when` check. `WhileSubscribed(5000)` ngừng collection khi không có subscriber, tiết kiệm tài nguyên. ### 19. Kiểm thử composable như thế nào? Compose cung cấp `ComposeTestRule` cho việc kiểm thử UI. Test được viết sử dụng semantic tree, không phải triển khai trực quan. ```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() } } ``` Sử dụng `testTag` trong Modifier để dễ dàng tìm node. Tránh kiểm thử dựa trên vị trí trực quan vì dễ bị hỏng khi thay đổi layout. ### 20. Tích hợp Compose với View XML hiện có như thế nào? Di chuyển dần từ XML sang Compose được thực hiện thông qua `ComposeView` (Compose bên trong XML) và `AndroidView` (XML bên trong Compose). Chiến lược này cho phép áp dụng Compose mà không cần viết lại toàn bộ ứng dụng. ```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() } ) } ``` Chiến lược di chuyển được khuyến nghị: bắt đầu với màn hình mới sử dụng Compose, sau đó di chuyển dần các màn hình hiện có từ leaf composable lên trên. ## Kết Luận 20 câu hỏi này bao gồm các khía cạnh then chốt của phỏng vấn Jetpack Compose: từ cơ bản khai báo đến các pattern kiến trúc sẵn sàng cho production. Chìa khóa thành công nằm ở sự hiểu biết sâu sắc về cơ chế recomposition, quản lý state đúng cách và kinh nghiệm thực tế xây dựng ứng dụng. ### Checklist Chuẩn Bị - Nắm vững sự khác biệt giữa Compose khai báo và XML mệnh lệnh - Hiểu cơ chế recomposition và smart recomposition - Luyện tập state hoisting và unidirectional data flow - Triển khai side effect đúng cách (LaunchedEffect, DisposableEffect) - Tối ưu LazyColumn với key và stable types - Nắm vững pattern ViewModel + UiState với sealed interface - Viết compose test sử dụng semantic tree - Hiểu interop Compose-XML cho việc di chuyển dần Thực hành trực tiếp với dự án thực tế vẫn là phương pháp hiệu quả nhất để làm chủ các khái niệm này. Mỗi câu hỏi được thảo luận trong hướng dẫn này đều đáng được khám phá sâu hơn với việc triển khai code thực tế. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/android/jetpack-compose-interview-questions