Chương 2: rememberUpdatedState – Giải Quyết Stale Callback Trong Coroutine, Effect Và AdMob Events
Giới thiệu
Một trong những lỗi khó phát hiện nhất trong Jetpack Compose là Stale State hoặc Stale Callback.
Ứng dụng vẫn chạy.
Không crash.
Không báo lỗi.
Nhưng logic hoạt động sai.
Đây là lý do rất nhiều Developer gặp các bug kiểu:
- Callback gọi dữ liệu cũ
- Timer không nhận state mới
- AdMob callback xử lý sai dữ liệu
- Coroutine vẫn giữ reference cũ
- Event bị mất sau recomposition
Compose cung cấp một API chuyên dụng để giải quyết vấn đề này:
rememberUpdatedState()
Stale Callback là gì?
Giả sử chúng ta có:
@Composable
fun SplashScreen(
onTimeout: () -> Unit
) {
LaunchedEffect(Unit) {
delay(3000)
onTimeout()
}
}
Thoạt nhìn hoàn toàn bình thường.
Nhưng hãy tưởng tượng:
T = 0s
onTimeout = A
Sau đó:
T = 1s
Screen recomposition
onTimeout = B
Coroutine vẫn đang chạy.
Sau 3 giây:
Coroutine gọi A
thay vì:
Coroutine gọi B
Đây chính là stale callback.
Vì sao xảy ra?
Khi LaunchedEffect được tạo:
LaunchedEffect(Unit)
nó capture toàn bộ biến đang tồn tại lúc đó.
Ví dụ:
LaunchedEffect(Unit) {
onTimeout()
}
thực chất tương tự:
val captured = onTimeout
launch {
captured()
}
Nếu callback thay đổi sau đó:
onTimeout = newCallback
Coroutine không biết điều này.
Giải pháp với rememberUpdatedState
@Composable
fun SplashScreen(
onTimeout: () -> Unit
) {
val currentOnTimeout by rememberUpdatedState(
onTimeout
)
LaunchedEffect(Unit) {
delay(3000)
currentOnTimeout()
}
}
Bây giờ:
Coroutine giữ nguyên
nhưng:
currentOnTimeout
luôn trỏ tới callback mới nhất.
Điều gì xảy ra bên trong?
Không dùng:
Coroutine
└── Callback A
Dù recomposition:
Callback B
Callback C
Callback D
Coroutine vẫn gọi:
A
Dùng rememberUpdatedState:
Coroutine
└── currentCallback
Mỗi lần recomposition:
currentCallback -> B
currentCallback -> C
currentCallback -> D
Coroutine luôn lấy giá trị mới nhất.
Ví dụ thực tế: Splash Screen
@Composable
fun SplashScreen(
navigateHome: () -> Unit
) {
val currentNavigateHome by rememberUpdatedState(
navigateHome
)
LaunchedEffect(Unit) {
delay(2000)
currentNavigateHome()
}
}
Đây là pattern được dùng rất nhiều trong production.
Ví dụ thực tế: Auto Refresh
@Composable
fun DashboardScreen(
refresh: suspend () -> Unit
) {
val currentRefresh by rememberUpdatedState(
refresh
)
LaunchedEffect(Unit) {
while (true) {
delay(60_000)
currentRefresh()
}
}
}
Nếu ViewModel thay đổi callback:
refresh = newRefresh
Coroutine vẫn sử dụng callback mới nhất.
Ví dụ thực tế: AdMob Rewarded Ads
Một bug khá phổ biến:
RewardedAd.show(
activity
) {
onRewardEarned(userId)
}
Giả sử:
userId = A
Sau đó user chuyển account:
userId = B
Reward callback được gọi muộn hơn.
Kết quả:
Reward cho A
thay vì:
Reward cho B
Giải pháp:
val currentUserId by rememberUpdatedState(
userId
)
RewardedAd.show(
activity
) {
rewardUser(currentUserId)
}
Ví dụ thực tế: Event Analytics
@Composable
fun AnalyticsTracker(
screenName: String
) {
val currentScreenName by rememberUpdatedState(
screenName
)
LaunchedEffect(Unit) {
delay(5000)
analytics.logScreen(
currentScreenName
)
}
}
Nếu user điều hướng rất nhanh:
Home
Profile
Settings
Analytics vẫn ghi nhận màn hình mới nhất.
Khi nào nên dùng?
Với LaunchedEffect
LaunchedEffect(Unit)
là trường hợp phổ biến nhất.
Với DisposableEffect
DisposableEffect(Unit)
Với SideEffect
SideEffect { }
Với callback SDK
Ví dụ:
- AdMob
- Firebase
- RevenueCat
- Facebook SDK
- AppsFlyer
Với Timer
delay()
ticker()
countdown()
Khi nào không nên dùng?
Nếu effect cần restart khi dữ liệu thay đổi.
Ví dụ:
LaunchedEffect(userId)
Trong trường hợp này:
userId
chính là key.
Bạn muốn coroutine khởi động lại.
Không cần:
rememberUpdatedState()
Sai lầm phổ biến
Sai lầm #1
Dùng cho mọi state.
val currentName by rememberUpdatedState(name)
Nhưng không dùng trong Effect nào.
=> Hoàn toàn vô nghĩa.
Sai lầm #2
Thay thế key của LaunchedEffect.
Sai:
val currentUser by rememberUpdatedState(user)
LaunchedEffect(Unit) {
loadUser(currentUser)
}
Nếu mục tiêu là reload user:
LaunchedEffect(user)
mới là giải pháp đúng.
Sai lầm #3
Nhầm lẫn với remember
remember { callback }
khác hoàn toàn:
rememberUpdatedState(callback)
remember
Giữ nguyên giá trị đầu tiên
rememberUpdatedState
Luôn cập nhật giá trị mới nhất
Quy tắc Senior Compose
Một nguyên tắc cực kỳ hữu ích:
Nếu Coroutine hoặc Effect tồn tại lâu hơn một lần recomposition và cần sử dụng dữ liệu mới nhất, hãy cân nhắc
rememberUpdatedState.
Đặc biệt với:
- Splash Screen
- Timer
- Analytics
- AdMob Callback
- SDK Listener
- Delayed Action
Kết luận
rememberUpdatedState là một API nhỏ nhưng cực kỳ quan trọng trong Compose.
Lợi ích:
- Tránh stale callback
- Tránh stale state
- Giảm bug khó phát hiện
- Giúp Coroutine luôn sử dụng dữ liệu mới nhất
- Hoạt động rất tốt với AdMob, Analytics và SDK bên thứ ba
Đây là một kỹ thuật mà rất nhiều Android Developer bỏ qua cho đến khi gặp bug trong production.
Top comments (0)