Hiệu ứng phụ là sự thay đổi về trạng thái của ứng dụng diễn ra bên ngoài phạm vi của một hàm kết hợp. Do vòng đời và các thuộc tính của thành phần kết hợp như các thành phần kết hợp lại không thể đoán trước, việc thực thi các thành phần kết hợp lại theo thứ tự khác nhau hoặc các thành phần kết hợp có thể bị loại bỏ, nên thành phần kết hợp tốt nhất là không có hiệu ứng phụ .
Tuy nhiên, đôi khi cũng cần có các hiệu ứng phụ, chẳng hạn để kích hoạt các sự kiện một lần như hiển thị thanh thông báo nhanh hoặc điều hướng đến một màn hình khác trong điều kiện trạng thái nhất định. Các hành động này nên được gọi từ một môi trường được kiểm soát và có nhận thức về vòng đời của thành phần kết hợp đó. Trong trang này, bạn sẽ tìm hiểu về một số hiệu ứng phụ khác của API Jetpack Compose.
Các trường hợp sử dụng trạng thái và hiệu ứng
Như đã nêu trong tài liệu Suy nghĩ trong Compose, các thành phần kết hợp không nên có hiệu ứng phụ (side-effect). Khi bạn cần thay đổi trạng thái của ứng dụng (như được mô tả trong tài liệu Tài liệu quản lý trạng thái), bạn nên sử dụng Effect API để những hiệu ứng phụ đó được thực hiện theo cách dễ dự đoán.
Do khả năng các hiệu ứng được mở ra trong ứng dụng Compose là khác nhau, chúng có thể dễ dàng bị lạm dụng. Hãy đảm bảo rằng công việc bạn thực hiện trong đó có liên quan đến giao diện người dùng và không làm hỏng luồng dữ liệu một chiều như đã giải thích trong tài liệu về Quản lý trạng thái.
LaunchedEffect: chạy các chức năng tạm ngưng trong phạm vi của một thành phần kết hợp
Để gọi các chức năng tạm ngưng một cách an toàn từ bên trong một thành phần kết hợp, hãy sử dụng thành phần kết hợp LaunchedEffect
Khi LaunchedEffect
nhập một thành phần, công cụ này sẽ khởi chạy
một coroutine với khối mã được truyền dưới dạng tham số. Coroutine sẽ bị huỷ nếu LaunchedEffect
thoát khỏi thành phần đó. Nếu LaunchedEffect
được kết hợp lại với các khoá khác nhau (xem phần Hiệu ứng khởi động lại bên dưới), coroutine hiện có sẽ bị huỷ và chức năng tạm ngưng mới sẽ được khởi chạy trong coroutine mới.
Ví dụ, việc hiển thị Snackbar
trong Scaffold
được hoàn tất với hàm SnackbarHostState.showSnackbar
, đây là chức năng tạm ngưng.
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
// If the UI state contains an error, show snackbar
if (state.hasError) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// `state.hasError` is false, and only start when `state.hasError` is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(scaffoldState = scaffoldState) {
/* ... */
}
}
Trong đoạn mã trên, một coroutine được kích hoạt nếu trạng thái đó chứa lỗi và sẽ được huỷ trong trường hợp không phải. Do nơi hàm được gọi LaunchedEffect
nằm trong một câu lệnh if, khi câu lệnh này sai, nếu LaunchedEffect
nằm trong
thành phần, thì câu lệnh đó sẽ bị xoá và do đó, coroutine sẽ bị huỷ.
rememberCoroutineScope: thu được một phạm vi nhận biết thành phần để khởi chạy coroutine bên ngoài một thành phần kết hợp
Do LaunchedEffect
là một hàm có thể kết hợp, nên bạn chỉ có thể sử dụng hàm này bên trong các hàm có thể kết hợp khác. Để khởi chạy một coroutine bên ngoài một thành phần kết hợp, nhưng trong phạm vi mà nội dung đó tự động được huỷ sau khi rời khỏi thành phần, hãy sử dụng rememberCoroutineScope
.
Ngoài ra, hãy sử dụng rememberCoroutineScope
bất cứ khi nào bạn cần kiểm soát vòng đời của một hoặc nhiều coroutine theo cách thủ công, ví dụ như việc huỷ một ảnh động khi sự kiện người dùng xảy ra.
rememberCoroutineScope
là một hàm có thể kết hợp có thể trả về giới hạn CoroutineScope
tới điểm thành phần được gọi. Phạm vi này sẽ bị huỷ khi cuộc gọi thoát khỏi thành phần đó.
Tiếp theo sau ví dụ trước, bạn có thể sử dụng mã này để hiển thị một Snackbar
khi người dùng nhấn vào một Button
:
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
rememberUpdatedState: tham chiếu một giá trị trong một hiệu ứng không nên khởi động lại nếu giá trị đó thay đổi
LaunchedEffect
khởi động lại khi một trong các tham số chính thay đổi. Tuy nhiên, trong một số trường hợp, bạn có thể muốn thu thập một giá trị trong hiệu ứng của mình. Nếu giá trị đó thay đổi, bạn không muốn hiệu ứng khởi động lại. Để làm điều này, bạn phải sử dụng rememberUpdatedState
để tạo tệp tham chiếu đến giá trị này. Giá trị này có thể được thu thập và cập nhật. Cách tiếp cận này hữu ích đối với những hiệu ứng chứa các thao tác dài hạn có thể gây tốn kém hoặc hạn chế khôi phục lại và bắt đầu lại.
Ví dụ, giả sử ứng dụng của bạn có một LandingScreen
biến mất sau một khoảng thời gian. Ngay cả khi LandingScreen
được kết hợp lại, không nên khởi chạy lại hiệu ứng cần chờ một thời gian và thông báo về thời gian đã trôi qua:
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes, the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
currentOnTimeout()
}
/* Landing screen content */
}
Để tạo một hiệu ứng phù hợp với vòng đời của nơi hàm được gọi, một hằng số không bao giờ thay đổi như Unit
hoặc true
sẽ được truyền dưới dạng một tham số. Trong mã trên, LaunchedEffect(true)
được sử dụng. Để đảm bảo rằng lambda onTimeout
luôn chứa giá trị mới nhấtLandingScreen
được kết hợp lại, onTimeout
cần được gói với hàm rememberUpdatedState
.
State
, currentOnTimeout
được trả về trong mã, phải được sử dụng trong hiệu ứng.
DisposableEffect: những hiệu ứng yêu cầu loại bỏ
Đối với các hiệu ứng phụ cần được loại bỏ sau khi các khoá thay đổi hoặc nếu thành phần kết hợp thoát khởi thành phần, hãy sử dụng DisposableEffect
.
Nếu các khoá DisposableEffect
thay đổi, thì thành phần kết hợp cần phải xử lý (thực hiện việc dọn dẹp) hiệu ứng hiện tại và đặt lại bằng cách gọi lại hiệu ứng đó.
Ví dụ, bạn có thể muốn gửi các sự kiện phân tích dựa trên các sự kiện Lifecycle
bằng cách sử dụng LifecycleObserver
.
Để theo dõi các sự kiện đó trong ứng dụng Compose, hãy sử dụng DisposableEffect
để đăng ký và
huỷ đăng ký người quan sát khi cần.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// Safely update the current lambdas when a new one is provided
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
// If `lifecycleOwner` changes, dispose and reset the effect
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
/* Home screen content */
}
Trong mã trên, hiệu ứng sẽ thêm observer
vào lifecycleOwner
. Nếu lifecycleOwner
thay đổi, hiệu ứng này sẽ được xử lý và được bắt đầu lại với lifecycleOwner
mới.
DisposableEffect
phải bao gồm mệnh đề onDispose
làm câu lệnh cuối cùng trong khối mã. Nếu không, IDE sẽ hiển thị lỗi thời gian tạo.
SideEffect: xuất bản Trạng thái Compose thành các mã không phải Compose
Để chia sẻ trạng thái Compose với các đối tượng không được quản lý bởi Compose, hãy sử dụng thành phần kết hợp SideEffect
vì nó được dẫn ra trong tất cả những lần tái cấu trúc thành công.
Ví dụ, thư viện phân tích của bạn có thể cho phép bạn phân đoạn dân số người dùng bằng cách đính kèm siêu dữ liệu tuỳ chỉnh ("thuộc tính người dùng" trong ví dụ này) vào tất cả các sự kiện phân tích tiếp theo. Để truyền đạt thông tin loại người dùng của người dùng hiện tại cho thư viện phân tích của bạn, hãy sử dụng SideEffect
để cập nhật giá trị.
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
productsState: chuyển trạng thái không phải Compose sang trạng thái Compose
produceState
khởi chạy một coroutine trong phạm vi mà thành phần có thể đẩy các giá trị vào State
được trả về. Sử dụng nó để chuyển đổi trạng thái không phải Compose sang trạng thái Compose, ví dụ như đưa trạng thái đăng ký bên ngoài như Flow
, LiveData
hoặc RxJava
vào thành phần.
Nguồn được khởi chạy khi produceState
tham gia thành phần và sẽ
bị huỷ khi thoát khỏi thành phần. Giá trị State
được trả về sẽ kết hợp; nên việc đặt cùng một giá trị sẽ không kích hoạt tái cấu trúc.
Mặc dù produceState
tạo một coroutine, nhưng nó cũng có thể được sử dụng để quan sát các nguồn dữ liệu không tạm ngưng. Để xoá gói đăng ký khỏi nguồn đó, hãy sử dụng hàm awaitDispose
.
Ví dụ sau cho biết cách sử dụng produceState
để tải hình ảnh từ mạng. Hàm có thể kết hợp loadNetworkImage
trả về một State
có thể
được sử dụng trong các thành phần kết hợp khác.
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<Image>> {
// Creates a State<T> with Result.Loading as initial value
// If either `url` or `imageRepository` changes, the running producer
// will cancel and will be re-launched with the new inputs.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// In a coroutine, can make suspend calls
val image = imageRepository.load(url)
// Update State with either an Error or Success result.
// This will trigger a recomposition where this State is read
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
derivedStateOf: chuyển đổi một hoặc nhiều đối tượng trạng thái thành trạng thái khác
Sử dụng
derivedStateOf
khi một trạng thái nhất định được tính toán hoặc lấy từ các đối tượng trạng thái khác. Việc sử dụng hàm này đảm bảo rằng việc tính toán sẽ chỉ xảy ra bất cứ khi nào một trong các trạng thái dùng trong phép tính thay đổi.
Ví dụ sau đây trình bày một danh sách Việc cần làm cơ bản có các tác vụ với từ khoá có mức độ ưu tiên cao do người dùng xác định xuất hiện đầu tiên:
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>() }
// Calculate high priority tasks only when the todoTasks or highPriorityKeywords
// change, not on every recomposition
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
}
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(highPriorityTasks) { /* ... */ }
items(todoTasks) { /* ... */ }
}
/* Rest of the UI where users can add elements to the list */
}
}
Trong đoạn mã trên, derivedStateOf
đảm bảo rằng bất cứ khi nào todoTasks
thay đổi, phép tính highPriorityTasks
sẽ được thực hiện và giao diện người dùng sẽ được cập nhật tương ứng. Nếu highPriorityKeywords
thay đổi, khối
remember
sẽ được thực thi và một đối tượng trạng thái phát sinh mới sẽ được tạo và được ghi nhớ thay cho khối cũ. Do quá trình lọc để tính toán
highPriorityTasks
có thể tốn kém, chỉ nên thực thi bộ lọc này khi bất kỳ điều gì của các danh sách này thay đổi, chứ không phải trên mọi tái cấu trúc.
Hơn nữa, việc cập nhật trạng thái bởi derivedStateOf
sẽ không gây ra thành phần kết hợp nơi nó được thông báo tái cấu trúc, Compose sẽ chỉ tái cấu trúc những thành phần kết hợp khi trạng thái hoàn lại của nó được đọc, trong LazyColumn
như trong ví dụ này.
Mã này cũng giả định rằng highPriorityKeywords
thay đổi ít hơn đáng kể so với todoTasks
. Nếu không phải trường hợp đó, mã này có thể sử dụng remember(todoTasks, highPriorityKeywords)
thay vì derivedStateOf
.
snapshotFlow: chuyển đổi Trạng thái của Compose thành Flows
Sử dụng snapshotFlow
để chuyển đổi các đối tượng State<T>
thành Flow lạnh. snapshotFlow
chạy khối của mình khi được thu thập và tạo ra
kết quả của các đối tượng State
được đọc trong đó. Khi một trong các đối tượng State
được đọc bên trong khối snapshotFlow
thay đổi, Flow sẽ tạo ra giá trị mới
cho bộ sưu tập của nó nếu giá trị mới không bằng giá trị được tạo ra trước đó (hành vi này tương tự như hành vi của
Flow.distinctUntilChanged
).
Ví dụ sau đây cho thấy một hiệu ứng phụ ghi lại thời điểm người dùng cuộn qua mục đầu tiên trong danh sách để gửi tới phân tích
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Trong mã trên, listState.firstVisibleItemIndex
được chuyển đổi thành một Flow có thể hưởng lợi từ sức mạnh của các toán tử của Flow.
Các hiệu ứng tái khởi động
Một số hiệu ứng trong ứng dụng Compose, như LaunchedEffect
, produceState
, hoặc
DisposableEffect
, sử dụng một số lượng biến số, các khoá để huỷ bỏ hiệu ứng đang chạy và bắt đầu một hiệu ứng mới bằng các khoá mới.
Biểu mẫu thông thường cho các API này là:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Do sự tinh vi của hành vi này, các sự cố có thể xảy ra nếu các tham số được sử dụng để khởi động lại hiệu ứng không phải là tham số phù hợp:
- Việc tái khởi động các hiệu ứng ít hơn mức cần thiết có thể gây ra lỗi trong ứng dụng của bạn.
- Việc tái khởi động các hiệu ứng nhiều hơn mức cần thiết có thể không hiệu quả.
Theo quy tắc chung, các biến có thể thay đổi và không thể thay đổi được dùng trong khối hiệu ứng của mã nên được thêm dưới dạng tham số vào thành phần kết hợp hiệu ứng. Ngoài những tham số đó,
bạn có thể thêm nhiều tham số hơn để thúc đẩy tái khởi động hiệu ứng. Nếu biến thay đổi không làm hiệu ứng khởi động lại, biến đó nên được bọc trong rememberUpdatedState
. Nếu biến không bao giờ
thay đổi do biến được bọc trong một remember
không có khoá, bạn không cần truyền biến dưới dạng khoá cho hiệu ứng đó.
Trong mã DisposableEffect
hiển thị ở trên, hiệu ứng này được lấy làm một tham số mà lifecycleOwner
đã sử dụng trong khối của nó, vì mọi thay đổi đối với chúng sẽ khiến hiệu ứng khởi động lại.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
Bạn không cần dùng khóa currentOnStart
và currentOnStop
làm khóa DisposableEffect
, vì giá trị của các khóa này không bao giờ thay đổi trong thành phần do việc sử dụng rememberUpdatedState
. Nếu bạn không truyền lifecycleOwner
dưới dạng tham số và tham số đó sẽ thay đổi, thì HomeScreen
sẽ tái cấu trúc nhưng DisposableEffect
không được xử lý và khởi động lại. Điều đó gây ra các sự cố vì lifecycleOwner
sai được sử dụng từ thời điểm đó trở đi.
Hằng số dưới dạng các khoá
Bạn có thể sử dụng một hằng số như true
làm khoá hiệu ứng để khiến nó tuân theo vòng đời của nơi hàm được gọi . Có các trường hợp sử dụng hợp lệ cho thuộc tính này, như ví dụ LaunchedEffect
ở trên. Tuy nhiên, trước khi làm điều đó,
hãy nghĩ kỹ và đảm bảo đó là điều bạn cần.