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 có khả năng 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 hàm tạm ngưng trong phạm vi của một thành phần kết hợp
Để thực hiện công việc trong suốt thời gian hoạt động của một thành phần kết hợp và có khả năng gọi các hàm tạm ngưng, 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ụ: sau đây là ảnh động tạo xung giá trị alpha với độ trễ có thể định cấu hình:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
Trong mã trên, ảnh động sử dụng hàm tạm ngưng delay
để chờ khoảng thời gian đã đặt. Sau đó, lớp này sẽ tạo ảnh động cho alpha từ 0 trở lại bằng cách sử dụng animateTo
.
Thao tác này sẽ lặp lại trong suốt vòng đời của thành phần kết hợp.
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(snackbarHostState: SnackbarHostState) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> Column(Modifier.padding(contentPadding)) { Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { 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 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
. Việc sử dụng SideEffect
đảm bảo rằng hiệu ứng sẽ thực thi sau mỗi lần kết hợp lại thành công. Mặt khác, việc thực hiện hiệu ứng trước khi đảm bảo quá trình kết hợp lại thành công là không chính xác. Đây là trường hợp khi bạn viết hiệu ứng trực tiếp trong một thành phần kết hợp.
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 rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // 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 }
produceState
: 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 = 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
Trong Compose, việc kết hợp lại (recomposition) xảy ra mỗi khi một đối tượng trạng thái được quan sát hoặc dữ liệu đầu vào của thành phần kết hợp thay đổi. Đối tượng trạng thái hoặc dữ liệu đầu vào có thể thay đổi thường xuyên hơn mức mà giao diện người dùng thực sự cần cập nhật, dẫn đến việc kết hợp lại một cách không cần thiết.
Bạn nên dùng hàm derivedStateOf
khi dữ liệu đầu vào cho một thành phần kết hợp thay đổi nhiều hơn mức cần thiết để kết hợp lại. Điều này thường xảy ra khi một nội dung nào đó thường xuyên thay đổi (ví dụ: một vị trí cuộn) nhưng thành phần kết hợp chỉ cần phản ứng lại với nội dung đó khi vượt qua một ngưỡng nhất định. derivedStateOf
tạo một đối tượng trạng thái Compose mới mà bạn có thể quan sát thấy rằng đối tượng này chỉ cập nhật theo tần suất cần thiết. Theo cách này, đối tượng này hoạt động tương tự như toán tử distinctUntilChanged()
của Luồng Kotlin.
Cách sử dụng đúng
Đoạn mã sau đây cho thấy một trường hợp sử dụng thích hợp cho derivedStateOf
:
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Trong đoạn mã, firstVisibleItemIndex
thay đổi bất cứ khi nào mục hiển thị đầu tiên thay đổi. Khi cuộn, giá trị này trở thành 0
, 1
, 2
, 3
, 4
, 5
, v.v. Tuy nhiên, chỉ cần kết hợp lại nếu giá trị đó lớn hơn 0
.
Sự không phù hợp về tần suất cập nhật này có nghĩa đây là trường hợp sử dụng tốt cho derivedStateOf
.
Cách sử dụng sai
Một sai lầm phổ biến là cho rằng nên sử dụng derivedStateOf
khi kết hợp hai đối tượng trạng thái Compose, vì bạn đang ở "trạng thái dẫn xuất". Tuy nhiên, việc này thuần tuý là hao tổn tài nguyên và không bắt buộc, như thể hiện trong đoạn mã sau:
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
Trong đoạn mã này, fullName
cần cập nhật với tần suất bằng với tần suất của firstName
và lastName
. Do đó, không xảy ra tình trạng kết hợp lại quá mức và việc sử dụng derivedStateOf
là không cần thiết.
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 thực hiện, hãy nghĩ kỹ và chắc chắn là bạn cần làm vậy.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Trạng thái và Jetpack Compose
- Kotlin cho Jetpack Compose
- Sử dụng Thành phần hiển thị trong Compose