Bỏ qua để đến nội dung

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.


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
@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


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

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:

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 freeidempotent (cho cùng input → cùng output).


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


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

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


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


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")
}
}

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")
}
}

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 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


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!


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

// 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