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
Phần tiêu đề “1. Compose Fundamentals”Q: Jetpack Compose là gì? Khác gì so với XML Views?
Phần tiêu đề “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.
| Aspect | XML Views | Compose |
|---|---|---|
| Paradigm | Imperative | Declarative |
| Language | XML + Kotlin/Java | Pure Kotlin |
| State Management | Manual (setText, setAdapter) | Automatic (Recomposition) |
| Preview | Separate preview file | @Preview trong code |
| Reusability | Custom Views (complex) | Functions (simple) |
| Performance | View invalidation | Smart recomposition |
// Compose - Declarative@Composablefun Greeting(name: String) { Text( text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium )}
// XML Views - ImperativetextView.text = "Hello, $name!"textView.textSize = 24fLợ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ì?
Phần tiêu đề “Q: @Composable annotation có ý nghĩa gì?”Trả lời:
@Composable annotation đánh dấu một function là composable function - function có thể:
- Emit UI elements
- Participate in recomposition
- Remember state
- Call other composable functions
@Composablefun 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-composablefun normalFunction() { UserCard(user) // Compile error!}Compose Compiler transforms @Composable functions để:
- Track state reads
- Enable smart recomposition
- Manage composition lifecycle
2. Recomposition
Phần tiêu đề “2. Recomposition”Q: Recomposition là gì?
Phần tiêu đề “Q: Recomposition là gì?”Trả lời:
Recomposition là process mà Compose re-executes composable functions khi state thay đổi để update UI.
@Composablefun Counter() { var count by remember { mutableStateOf(0) } // State
Button(onClick = { count++ }) { // State change triggers recomposition Text("Count: $count") }}Recomposition Flow:
flowchart LR A["State Change"] --> B["Mark composables<br/>reading state"] B --> C["Re-execute<br/>composables"] C --> D["Update UI"]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 free và idempotent (cho cùng input → cùng output).
Q: Làm sao để skip unnecessary recomposition?
Phần tiêu đề “Q: Làm sao để skip unnecessary recomposition?”Trả lời:
1. Use stable/immutable types:
// ✅ Stable - Compose can skip recompositiondata class User(val id: Int, val name: String)
// ❌ Unstable - Always recomposesclass User(var name: String) // Mutable property2. Use @Stable or @Immutable:
@Stableclass UserState(initialName: String) { var name by mutableStateOf(initialName)}
@Immutabledata class UiConfig(val theme: String, val locale: String)3. Extract smaller composables:
// ❌ Bad - entire list recomposes@Composablefun UserList(users: List<User>, selectedId: Int) { LazyColumn { items(users) { user -> UserItem(user, isSelected = user.id == selectedId) } }}
// ✅ Better - use key to help Compose track items@Composablefun 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:
@Composablefun 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
Phần tiêu đề “3. State Management”Q: remember vs rememberSaveable?
Phần tiêu đề “Q: remember vs rememberSaveable?”Trả lời:
| Function | Survives Recomposition | Survives Config Change | Survives Process Death |
|---|---|---|---|
remember |
✅ | ❌ | ❌ |
rememberSaveable |
✅ | ✅ | ✅ |
@Composablefun 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 Saverval 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ì?
Phần tiêu đề “Q: State hoisting là gì?”Trả lời:
State hoisting là pattern move state up từ composable con lên composable cha để:
- Make composable stateless và reusable
- Enable single source of truth
- Allow parent to control child’s state
// ❌ Stateful - hard to reuse and test@Composablefun LoginButton() { var isLoading by remember { mutableStateOf(false) }
Button( onClick = { isLoading = true }, enabled = !isLoading ) { if (isLoading) CircularProgressIndicator() else Text("Login") }}
// ✅ Stateless - state hoisted to parent@Composablefun LoginButton( isLoading: Boolean, onLoginClick: () -> Unit) { Button( onClick = onLoginClick, enabled = !isLoading ) { if (isLoading) CircularProgressIndicator() else Text("Login") }}
// Parent controls state@Composablefun 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?
Phần tiêu đề “Q: StateFlow vs LiveData trong Compose?”Trả lời:
| Aspect | StateFlow | LiveData |
|---|---|---|
| Kotlin-first | ✅ | ❌ (Java-based) |
| Null safety | ✅ (requires initial value) | ❌ (can be null) |
| Lifecycle | Manual with collectAsStateWithLifecycle |
Auto with observeAsState |
| Performance | Better with coroutines | Good |
| Recommended | ✅ For new projects | Legacy projects |
// ViewModel with StateFlow (Recommended)class MainViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow()}
// Compose UI@Composablefun 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
Phần tiêu đề “4. Side Effects”Q: Side effect APIs trong Compose?
Phần tiêu đề “Q: Side effect APIs trong Compose?”Trả lời:
Side effects là operations xảy ra ngoài composable function (network calls, logging, navigation).
| API | Use Case |
|---|---|
LaunchedEffect |
Coroutine scope tied to composition |
DisposableEffect |
Cleanup when leaving composition |
SideEffect |
Publish state to non-compose code |
rememberCoroutineScope |
Coroutine scope controlled by events |
derivedStateOf |
Compute value from other states |
produceState |
Convert non-compose state to compose state |
@Composablefun 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?
Phần tiêu đề “Q: LaunchedEffect vs rememberCoroutineScope?”Trả lời:
| Aspect | LaunchedEffect | rememberCoroutineScope |
|---|---|---|
| Trigger | On composition/key change | On user events |
| Lifecycle | Tied to composition | Tied to composition |
| Cancel on recomposition | ✅ (with key change) | ❌ |
| Use case | Load data, subscribe to flows | Button clicks, gestures |
@Composablefun 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
Phần tiêu đề “5. Composition & Modifiers”Q: Modifier là gì? Thứ tự quan trọng không?
Phần tiêu đề “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 smallerBox( modifier = Modifier .padding(16.dp) .clickable { /* ... */ })
// ✅ Padding outside clickable - touch area largerBox( modifier = Modifier .clickable { /* ... */ } .padding(16.dp))
// Background then padding - bg covers paddingBox( modifier = Modifier .background(Color.Blue) .padding(16.dp) // Padding on blue background)
// Padding then background - no bg in padding areaBox( 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ì?
Phần tiêu đề “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 CompositionLocalval LocalUserSession = compositionLocalOf<UserSession> { error("No UserSession provided")}
// Provide value at top level@Composablefun App() { CompositionLocalProvider(LocalUserSession provides userSession) { MainContent() }}
// Access anywhere in subtree@Composablefun UserAvatar() { val session = LocalUserSession.current // Access without parameters
AsyncImage( model = session.user.avatarUrl, contentDescription = null )}Built-in CompositionLocals:
LocalContext- Android ContextLocalConfiguration- Device configurationLocalDensity- Screen density
⚠️ Cảnh báo: Overusing CompositionLocal makes code harder to understand. Use sparingly!
6. Performance
Phần tiêu đề “6. Performance”Q: Làm sao optimize performance trong Compose?
Phần tiêu đề “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:
@Composablefun 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@Composablefun BadButton(viewModel: ViewModel) { Button(onClick = { viewModel.doSomething() }) { }}
// ✅ Stable reference with remember@Composablefun 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
Phần tiêu đề “📝 Quick Reference”State Pattern Cheat Sheet:
Phần tiêu đề “State Pattern Cheat Sheet:”// UI Statevar value by remember { mutableStateOf(initial) }
// Persist across config changevar value by rememberSaveable { mutableStateOf(initial) }
// Derived stateval derived by remember { derivedStateOf { compute(value) } }
// Flow to Stateval state by flow.collectAsStateWithLifecycle()
// One-time effectLaunchedEffect(key) { doSomething() }
// Cleanup effectDisposableEffect(key) { onDispose { cleanup() } }📚 Tìm hiểu thêm: Compose Basics, Compose Advanced