DEV Community

ViO Tech
ViO Tech

Posted on

Jetpack Compose Senior Handbook 13 Kỹ Thuật Jetpack Compose Mà Android Senior Developer Sử Dụng Hàng Ngày

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(...)
Enter fullscreen mode Exit fullscreen mode

hoặc

editText.doAfterTextChanged(...)
Enter fullscreen mode Exit fullscreen mode

Nhưng trong Compose:

LazyListState
MutableState
PagerState
ScrollState
Enter fullscreen mode Exit fullscreen mode

đề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()
Enter fullscreen mode Exit fullscreen mode

Đâ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()
Enter fullscreen mode Exit fullscreen mode

Bạn muốn log analytics:

User reached item 50
Enter fullscreen mode Exit fullscreen mode

Nhiều người sẽ viết:

LaunchedEffect(
    listState.firstVisibleItemIndex
) {
    analytics.log(
        listState.firstVisibleItemIndex
    )
}
Enter fullscreen mode Exit fullscreen mode

Điều này hoạt động.

Nhưng mỗi lần index thay đổi:

1
2
3
4
5
6
7
8
...
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Mỗi lần:

count++
Enter fullscreen mode Exit fullscreen mode

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

        }
}
Enter fullscreen mode Exit fullscreen mode

Compose sẽ:

  1. Theo dõi state bên trong block
  2. Phát ra giá trị mới khi state thay đổi
  3. Chuyển thành Flow

Tương tự:

Flow<Int>
Enter fullscreen mode Exit fullscreen mode

Ví dụ cơ bản

@Composable
fun HomeScreen() {

    val listState =
        rememberLazyListState()

    LaunchedEffect(Unit) {

        snapshotFlow {
            listState.firstVisibleItemIndex
        }
            .collect { index ->

                Log.d(
                    "Scroll",
                    "Current index: $index"
                )
            }
    }
}
Enter fullscreen mode Exit fullscreen mode

Mỗi lần scroll:

0
1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
LaunchedEffect(Unit) {

    snapshotFlow {
        listState.firstVisibleItemIndex
    }
        .filter {
            it >= 20
        }
        .take(1)
        .collect {

            analytics.logEvent(
                "Reached_20_Items"
            )
        }
}
Enter fullscreen mode Exit fullscreen mode

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()
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

Khi người dùng đến gần cuối danh sách:

Total = 100
Current = 95
Enter fullscreen mode Exit fullscreen mode

Tự động load thêm dữ liệu.


Tối ưu với distinctUntilChanged

Không nên:

snapshotFlow {
    listState.firstVisibleItemIndex
}
.collect {

}
Enter fullscreen mode Exit fullscreen mode

Bởi vì:

5
5
5
5
5
Enter fullscreen mode Exit fullscreen mode

có thể được emit nhiều lần.

Tối ưu:

snapshotFlow {
    listState.firstVisibleItemIndex
}
.distinctUntilChanged()
.collect {

}
Enter fullscreen mode Exit fullscreen mode

Chỉ xử lý khi giá trị thực sự thay đổi.


Ví dụ thực tế: Search Debounce

var query by remember {
    mutableStateOf("")
}
Enter fullscreen mode Exit fullscreen mode
LaunchedEffect(Unit) {

    snapshotFlow {
        query
    }
        .debounce(500)
        .distinctUntilChanged()
        .collect {

            viewModel.search(it)
        }
}
Enter fullscreen mode Exit fullscreen mode

Người dùng nhập:

h
he
hel
hell
hello
Enter fullscreen mode Exit fullscreen mode

API chỉ được gọi sau khi dừng nhập.


Ví dụ thực tế: AdMob Impression Tracking

Giả sử:

Native Ad
Enter fullscreen mode Exit fullscreen mode

xuất hiện tại vị trí:

Item 15
Enter fullscreen mode Exit fullscreen mode

Theo dõi:

LaunchedEffect(Unit) {

    snapshotFlow {
        listState.firstVisibleItemIndex
    }
        .filter {
            it >= 15
        }
        .take(1)
        .collect {

            analytics.logEvent(
                "Ad_Impression"
            )
        }
}
Enter fullscreen mode Exit fullscreen mode

Đả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
            )
        }
}
Enter fullscreen mode Exit fullscreen mode

Rất phù hợp với MVI.


Kết hợp với MVI

State:

data class HomeState(
    val showFab: Boolean = false
)
Enter fullscreen mode Exit fullscreen mode

Event:

sealed interface HomeIntent {

    data class FabVisibilityChanged(
        val visible: Boolean
    ) : HomeIntent
}
Enter fullscreen mode Exit fullscreen mode

UI:

LaunchedEffect(Unit) {

    snapshotFlow {
        listState.firstVisibleItemIndex
    }
        .map {
            it > 10
        }
        .distinctUntilChanged()
        .collect {

            viewModel.dispatch(
                HomeIntent.FabVisibilityChanged(
                    it
                )
            )
        }
}
Enter fullscreen mode Exit fullscreen mode

Đâ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 {}
Enter fullscreen mode Exit fullscreen mode

Có thể gây:

  • Spam analytics
  • Spam API
  • Spam event

Sai lầm #2

Quan sát quá nhiều state

snapshotFlow {
    hugeObject
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Nếu dữ liệu đã là:

StateFlow
Enter fullscreen mode Exit fullscreen mode

thì không cần snapshotFlow.


Khi nào nên dùng?

Scroll Tracking

LazyListState
Enter fullscreen mode Exit fullscreen mode

Pager Tracking

PagerState
Enter fullscreen mode Exit fullscreen mode

Search Debounce

TextField
Enter fullscreen mode Exit fullscreen mode

Analytics

Screen Tracking
Enter fullscreen mode Exit fullscreen mode

Ad Impression

Banner
Native Ad
Enter fullscreen mode Exit fullscreen mode

Infinite Pagination

Load More
Enter fullscreen mode Exit fullscreen mode

Khi nào không nên dùng?

Không nên dùng để render UI.

Sai:

snapshotFlow {
    count
}
.collect {
    Text("$it")
}
Enter fullscreen mode Exit fullscreen mode

Compose State đã xử lý việc render.

snapshotFlow dành cho:

Side Effects
Enter fullscreen mode Exit fullscreen mode

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)