Skip to Content

Câu hỏi phỏng vấn Jetpack Compose

Phần này tổng hợp các câu hỏi về Jetpack Compose - modern UI toolkit của Android, dựa trên sách Manifest Android Interview.


1. Compose Fundamentals

Q: Jetpack Compose là gì? Khác gì so với XML Views?

Trả lời:

Jetpack Compose là modern toolkit để build native Android UI, sử dụng declarative approach với Kotlin.

AspectXML ViewsCompose
ParadigmImperativeDeclarative
LanguageXML + Kotlin/JavaPure Kotlin
State ManagementManual (setText, setAdapter)Automatic (Recomposition)
PreviewSeparate preview file@Preview trong code
ReusabilityCustom Views (complex)Functions (simple)
PerformanceView invalidationSmart recomposition
// Compose - Declarative @Composable fun Greeting(name: String) { Text( text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium ) } // XML Views - Imperative textView.text = "Hello, $name!" textView.textSize = 24f

Lợi ích của Compose:

  • ✅ Less code, more readable
  • ✅ Powerful previews
  • ✅ State-driven UI
  • ✅ Kotlin-first APIs

📚 Tìm hiểu thêm: Giới thiệu Jetpack Compose


Q: @Composable annotation có ý nghĩa gì?

Trả lời:

@Composable annotation đánh dấu một function là composable function - function có thể:

  1. Emit UI elements
  2. Participate in recomposition
  3. Remember state
  4. Call other composable functions
@Composable fun UserCard(user: User) { // Can call other composables Card(modifier = Modifier.padding(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { // Emit UI elements AsyncImage(model = user.avatar, contentDescription = null) Text(text = user.name) } } } // ❌ Cannot call composable from non-composable fun normalFunction() { UserCard(user) // Compile error! }

Compose Compiler transforms @Composable functions để:

  • Track state reads
  • Enable smart recomposition
  • Manage composition lifecycle

2. Recomposition

Q: Recomposition là gì?

Trả lời:

Recomposition là process mà Compose re-executes composable functions khi state thay đổi để update UI.

@Composable fun Counter() { var count by remember { mutableStateOf(0) } // State Button(onClick = { count++ }) { // State change triggers recomposition Text("Count: $count") } }

Recomposition Flow:

Key characteristics:

  • Smart: Chỉ recompose composables đọc state thay đổi
  • Optimistic: Có thể skip hoặc cancel nếu state đã cũ
  • Not guaranteed: Có thể chạy nhiều lần hoặc bị cancel

⚠️ Quan trọng: Composable functions phải side-effect freeidempotent (cho cùng input → cùng output).


Q: Làm sao để skip unnecessary recomposition?

Trả lời:

1. Use stable/immutable types:

// ✅ Stable - Compose can skip recomposition data class User(val id: Int, val name: String) // ❌ Unstable - Always recomposes class User(var name: String) // Mutable property

2. Use @Stable or @Immutable:

@Stable class UserState(initialName: String) { var name by mutableStateOf(initialName) } @Immutable data class UiConfig(val theme: String, val locale: String)

3. Extract smaller composables:

// ❌ Bad - entire list recomposes @Composable fun UserList(users: List<User>, selectedId: Int) { LazyColumn { items(users) { user -> UserItem(user, isSelected = user.id == selectedId) } } } // ✅ Better - use key to help Compose track items @Composable fun UserList(users: List<User>, selectedId: Int) { LazyColumn { items(users, key = { it.id }) { user -> UserItem(user, isSelected = user.id == selectedId) } } }

4. Use derivedStateOf for computed values:

@Composable fun FilteredList(items: List<Item>, query: String) { // ✅ Only recomputes when items or query changes val filteredItems by remember(items, query) { derivedStateOf { items.filter { it.name.contains(query) } } } LazyColumn { items(filteredItems) { ItemRow(it) } } }

📚 Tìm hiểu thêm: Compose State


3. State Management

Q: remember vs rememberSaveable?

Trả lời:

FunctionSurvives RecompositionSurvives Config ChangeSurvives Process Death
remember
rememberSaveable
@Composable fun SearchScreen() { // ❌ Lost on rotation var query by remember { mutableStateOf("") } // ✅ Survives rotation and process death var query by rememberSaveable { mutableStateOf("") } // For custom objects, use Saver var user by rememberSaveable(stateSaver = UserSaver) { mutableStateOf(User()) } } // Custom Saver val UserSaver = Saver<User, Bundle>( save = { user -> Bundle().apply { putString("name", user.name) } }, restore = { bundle -> User(bundle.getString("name") ?: "") } )

Khi nào dùng gì:

  • remember: UI state không cần persist (animation state, scroll position)
  • rememberSaveable: User input, form data, important UI state

Q: State hoisting là gì?

Trả lời:

State hoisting là pattern move state up từ composable con lên composable cha để:

  • Make composable statelessreusable
  • Enable single source of truth
  • Allow parent to control child’s state
// ❌ Stateful - hard to reuse and test @Composable fun LoginButton() { var isLoading by remember { mutableStateOf(false) } Button( onClick = { isLoading = true }, enabled = !isLoading ) { if (isLoading) CircularProgressIndicator() else Text("Login") } } // ✅ Stateless - state hoisted to parent @Composable fun LoginButton( isLoading: Boolean, onLoginClick: () -> Unit ) { Button( onClick = onLoginClick, enabled = !isLoading ) { if (isLoading) CircularProgressIndicator() else Text("Login") } } // Parent controls state @Composable fun LoginScreen(viewModel: LoginViewModel) { val isLoading by viewModel.isLoading.collectAsState() LoginButton( isLoading = isLoading, onLoginClick = { viewModel.login() } ) }

Pattern: (state, onEvent) -> Unit


Q: StateFlow vs LiveData trong Compose?

Trả lời:

AspectStateFlowLiveData
Kotlin-first❌ (Java-based)
Null safety✅ (requires initial value)❌ (can be null)
LifecycleManual with collectAsStateWithLifecycleAuto with observeAsState
PerformanceBetter with coroutinesGood
Recommended✅ For new projectsLegacy projects
// ViewModel with StateFlow (Recommended) class MainViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow() } // Compose UI @Composable fun MainScreen(viewModel: MainViewModel) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when (uiState) { is UiState.Loading -> LoadingIndicator() is UiState.Success -> Content(uiState.data) is UiState.Error -> ErrorMessage(uiState.message) } }

📚 Tìm hiểu thêm: Flow trong Android


4. Side Effects

Q: Side effect APIs trong Compose?

Trả lời:

Side effects là operations xảy ra ngoài composable function (network calls, logging, navigation).

APIUse Case
LaunchedEffectCoroutine scope tied to composition
DisposableEffectCleanup when leaving composition
SideEffectPublish state to non-compose code
rememberCoroutineScopeCoroutine scope controlled by events
derivedStateOfCompute value from other states
produceStateConvert non-compose state to compose state
@Composable fun UserProfile(userId: String) { var user by remember { mutableStateOf<User?>(null) } // LaunchedEffect - runs when userId changes LaunchedEffect(userId) { user = api.fetchUser(userId) } // DisposableEffect - cleanup on dispose DisposableEffect(Unit) { val listener = analytics.trackScreen("profile") onDispose { listener.stop() } } // rememberCoroutineScope - for event handlers val scope = rememberCoroutineScope() Button(onClick = { scope.launch { api.updateUser(user) } }) { Text("Save") } }

Q: LaunchedEffect vs rememberCoroutineScope?

Trả lời:

AspectLaunchedEffectrememberCoroutineScope
TriggerOn composition/key changeOn user events
LifecycleTied to compositionTied to composition
Cancel on recomposition✅ (with key change)
Use caseLoad data, subscribe to flowsButton clicks, gestures
@Composable fun DataScreen(id: String) { // LaunchedEffect - automatic, triggered by id change LaunchedEffect(id) { viewModel.loadData(id) // Auto-cancelled when id changes } val scope = rememberCoroutineScope() // rememberCoroutineScope - manual, triggered by click Button(onClick = { scope.launch { viewModel.refresh() // Not cancelled on recomposition } }) { Text("Refresh") } }

5. Composition & Modifiers

Q: Modifier là gì? Thứ tự quan trọng không?

Trả lời:

Modifier là objects để decorate/augment composables. Thứ tự RẤT quan trọng!

// ❌ Padding inside clickable - touch area smaller Box( modifier = Modifier .padding(16.dp) .clickable { /* ... */ } ) // ✅ Padding outside clickable - touch area larger Box( modifier = Modifier .clickable { /* ... */ } .padding(16.dp) ) // Background then padding - bg covers padding Box( modifier = Modifier .background(Color.Blue) .padding(16.dp) // Padding on blue background ) // Padding then background - no bg in padding area Box( modifier = Modifier .padding(16.dp) .background(Color.Blue) // Blue only inside padding )

Common modifiers:

Modifier .fillMaxSize() // Fill parent .size(100.dp) // Fixed size .padding(16.dp) // Add spacing .background(Color.Gray) // Background color .clip(RoundedCornerShape(8.dp)) // Clip shape .clickable { } // Handle clicks .border(1.dp, Color.Black) // Border

📚 Tìm hiểu thêm: Compose Layouts


Q: CompositionLocal là gì?

Trả lời:

CompositionLocal là cách implicitly pass data xuống composition tree mà không cần truyền qua parameters.

// Define CompositionLocal val LocalUserSession = compositionLocalOf<UserSession> { error("No UserSession provided") } // Provide value at top level @Composable fun App() { CompositionLocalProvider(LocalUserSession provides userSession) { MainContent() } } // Access anywhere in subtree @Composable fun UserAvatar() { val session = LocalUserSession.current // Access without parameters AsyncImage( model = session.user.avatarUrl, contentDescription = null ) }

Built-in CompositionLocals:

  • LocalContext - Android Context
  • LocalConfiguration - Device configuration
  • LocalDensity - Screen density

⚠️ Cảnh báo: Overusing CompositionLocal makes code harder to understand. Use sparingly!


6. Performance

Q: Làm sao optimize performance trong Compose?

Trả lời:

1. Use keys in LazyColumn/LazyRow:

LazyColumn { items(users, key = { it.id }) { user -> UserItem(user) } }

2. Use remember for expensive calculations:

@Composable fun FilteredList(items: List<Item>) { val sortedItems = remember(items) { items.sortedBy { it.name } // Only sorts when items change } }

3. Use derivedStateOf for frequently changing states:

val scrollState = rememberScrollState() val showButton by remember { derivedStateOf { scrollState.value > 100 } // Only recalculates when condition changes }

4. Avoid lambda allocations:

// ❌ New lambda on each recomposition @Composable fun BadButton(viewModel: ViewModel) { Button(onClick = { viewModel.doSomething() }) { } } // ✅ Stable reference with remember @Composable fun GoodButton(viewModel: ViewModel) { val onClick = remember { { viewModel.doSomething() } } Button(onClick = onClick) { } }

5. Use Layout Inspector to debug:

  • Android Studio → Tools → Layout Inspector
  • Shows recomposition counts

📝 Quick Reference

State Pattern Cheat Sheet:

// UI State var value by remember { mutableStateOf(initial) } // Persist across config change var value by rememberSaveable { mutableStateOf(initial) } // Derived state val derived by remember { derivedStateOf { compute(value) } } // Flow to State val state by flow.collectAsStateWithLifecycle() // One-time effect LaunchedEffect(key) { doSomething() } // Cleanup effect DisposableEffect(key) { onDispose { cleanup() } }

📚 Tìm hiểu thêm: Compose Basics, Compose Advanced

Last updated on