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.
| 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
@Composable
fun Greeting(name: String) {
Text(
text = "Hello, $name!",
style = MaterialTheme.typography.headlineMedium
)
}
// XML Views - Imperative
textView.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ì?
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
@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 free và idempotent (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 property2. 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:
| Function | Survives Recomposition | Survives Config Change | Survives 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 stateless và reusable
- 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:
| 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
@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).
| 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 |
@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:
| 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 |
@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 ContextLocalConfiguration- Device configurationLocalDensity- 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