Chương 3: snapshotFlow – Kết Nối Compose State Với Kotlin Flow Một Cách Chuẩn Production
Giới thiệu
Khi chuyển từ View System sang Jetpack Compose, nhiều Android Developer thường gặp một câu hỏi:
Làm sao để lắng nghe sự thay đổi của Compose State giống như Listener hoặc Flow?
Trong View System chúng ta thường làm như sau:
recyclerView.addOnScrollListener(...)
hoặc
editText.doAfterTextChanged(...)
Nhưng trong Compose:
LazyListState
MutableState
PagerState
ScrollState
đều hoạt động theo cơ chế Snapshot System.
Compose cung cấp một API rất mạnh để chuyển Snapshot State thành Kotlin Flow:
snapshotFlow()
Đây là một trong những API được sử dụng nhiều nhất trong:
- Analytics Tracking
- Infinite Pagination
- Ad Impression Tracking
- Search Debounce
- Scroll Monitoring
- MVI Side Effects
Vấn đề
Giả sử bạn muốn biết người dùng đã scroll đến đâu.
val listState = rememberLazyListState()
Bạn muốn log analytics:
User reached item 50
Nhiều người sẽ viết:
LaunchedEffect(
listState.firstVisibleItemIndex
) {
analytics.log(
listState.firstVisibleItemIndex
)
}
Điều này hoạt động.
Nhưng mỗi lần index thay đổi:
1
2
3
4
5
6
7
8
...
Compose sẽ restart Effect liên tục.
Không tối ưu.
Snapshot System là gì?
Compose quản lý state bằng Snapshot.
Ví dụ:
var count by remember {
mutableStateOf(0)
}
Mỗi lần:
count++
Snapshot được cập nhật.
Compose biết chính xác state nào thay đổi.
snapshotFlow cho phép quan sát những thay đổi này dưới dạng Flow.
Cách hoạt động của snapshotFlow
LaunchedEffect(Unit) {
snapshotFlow {
listState.firstVisibleItemIndex
}
.collect { index ->
}
}
Compose sẽ:
- Theo dõi state bên trong block
- Phát ra giá trị mới khi state thay đổi
- Chuyển thành Flow
Tương tự:
Flow<Int>
Ví dụ cơ bản
@Composable
fun HomeScreen() {
val listState =
rememberLazyListState()
LaunchedEffect(Unit) {
snapshotFlow {
listState.firstVisibleItemIndex
}
.collect { index ->
Log.d(
"Scroll",
"Current index: $index"
)
}
}
}
Mỗi lần scroll:
0
1
2
3
4
5
Flow sẽ emit giá trị mới.
Ví dụ thực tế: Analytics Tracking
Bạn muốn ghi nhận:
User reached item 20
LaunchedEffect(Unit) {
snapshotFlow {
listState.firstVisibleItemIndex
}
.filter {
it >= 20
}
.take(1)
.collect {
analytics.logEvent(
"Reached_20_Items"
)
}
}
Event chỉ được gửi một lần.
Ví dụ thực tế: Infinite Pagination
Đây là use case phổ biến nhất.
LaunchedEffect(Unit) {
snapshotFlow {
listState.layoutInfo
.visibleItemsInfo
.lastOrNull()
?.index
}
.collect { lastVisible ->
if (
lastVisible != null &&
lastVisible >= items.size - 5
) {
viewModel.loadNextPage()
}
}
}
Khi người dùng đến gần cuối danh sách:
Total = 100
Current = 95
Tự động load thêm dữ liệu.
Tối ưu với distinctUntilChanged
Không nên:
snapshotFlow {
listState.firstVisibleItemIndex
}
.collect {
}
Bởi vì:
5
5
5
5
5
có thể được emit nhiều lần.
Tối ưu:
snapshotFlow {
listState.firstVisibleItemIndex
}
.distinctUntilChanged()
.collect {
}
Chỉ xử lý khi giá trị thực sự thay đổi.
Ví dụ thực tế: Search Debounce
var query by remember {
mutableStateOf("")
}
LaunchedEffect(Unit) {
snapshotFlow {
query
}
.debounce(500)
.distinctUntilChanged()
.collect {
viewModel.search(it)
}
}
Người dùng nhập:
h
he
hel
hell
hello
API chỉ được gọi sau khi dừng nhập.
Ví dụ thực tế: AdMob Impression Tracking
Giả sử:
Native Ad
xuất hiện tại vị trí:
Item 15
Theo dõi:
LaunchedEffect(Unit) {
snapshotFlow {
listState.firstVisibleItemIndex
}
.filter {
it >= 15
}
.take(1)
.collect {
analytics.logEvent(
"Ad_Impression"
)
}
}
Đảm bảo impression chỉ ghi nhận một lần.
Ví dụ thực tế: FAB Visibility
LaunchedEffect(Unit) {
snapshotFlow {
listState.firstVisibleItemIndex
}
.map {
it > 10
}
.distinctUntilChanged()
.collect { visible ->
viewModel.updateFabVisibility(
visible
)
}
}
Rất phù hợp với MVI.
Kết hợp với MVI
State:
data class HomeState(
val showFab: Boolean = false
)
Event:
sealed interface HomeIntent {
data class FabVisibilityChanged(
val visible: Boolean
) : HomeIntent
}
UI:
LaunchedEffect(Unit) {
snapshotFlow {
listState.firstVisibleItemIndex
}
.map {
it > 10
}
.distinctUntilChanged()
.collect {
viewModel.dispatch(
HomeIntent.FabVisibilityChanged(
it
)
)
}
}
Đây là pattern rất phổ biến trong production.
Sai lầm phổ biến
Sai lầm #1
Quên distinctUntilChanged
snapshotFlow {
state
}
.collect {}
Có thể gây:
- Spam analytics
- Spam API
- Spam event
Sai lầm #2
Quan sát quá nhiều state
snapshotFlow {
hugeObject
}
Mỗi thay đổi nhỏ đều phát sinh emission.
Nên chỉ lấy dữ liệu cần thiết.
Sai lầm #3
Dùng thay thế StateFlow
Sai:
snapshotFlow {
uiState
}
Nếu dữ liệu đã là:
StateFlow
thì không cần snapshotFlow.
Khi nào nên dùng?
Scroll Tracking
LazyListState
Pager Tracking
PagerState
Search Debounce
TextField
Analytics
Screen Tracking
Ad Impression
Banner
Native Ad
Infinite Pagination
Load More
Khi nào không nên dùng?
Không nên dùng để render UI.
Sai:
snapshotFlow {
count
}
.collect {
Text("$it")
}
Compose State đã xử lý việc render.
snapshotFlow dành cho:
Side Effects
không phải UI Rendering.
Quy tắc Senior Compose
Một nguyên tắc rất quan trọng:
Nếu bạn muốn biến Compose State thành Kotlin Flow để thực hiện Analytics, Pagination hoặc Side Effects, hãy sử dụng
snapshotFlow.
Compose State dùng cho UI.
Flow dùng cho Event Stream.
snapshotFlow là cây cầu nối giữa hai thế giới đó.
Kết luận
snapshotFlow là một trong những API mạnh nhất của Compose.
Lợi ích:
- Chuyển Compose State thành Flow
- Theo dõi scroll hiệu quả
- Hỗ trợ pagination
- Tích hợp analytics dễ dàng
- Theo dõi Ad Impression
- Hoạt động rất tốt với MVI
Trong các dự án Compose production, đây gần như là lựa chọn mặc định mỗi khi cần theo dõi sự thay đổi của UI State để thực hiện Side Effects.
Top comments (0)