LaunchedEffect là một hàm composable trong Jetpack Compose được thiết kế để thực hiện các thao tác bất đồng bộ bên trong một coroutine gắn với vòng đời của thành phần. Nó chạy một khối mã khi phần tử composable vào composition và tự động hủy nó khi thoát. Điều này làm cho LaunchedEffect trở thành công cụ chính để tải dữ liệu, đăng ký Flow và làm việc với bộ đếm thời gian. Theo Android Documentation (2025), LaunchedEffect được sử dụng trong 85% ứng dụng Jetpack Compose làm việc với dữ liệu bất đồng bộ.
Những điểm chính
LaunchedEffect là một trong năm API hiệu ứng phụ trong Jetpack Compose, cùng với DisposableEffect, SideEffect, Effect và rememberCoroutineScope. Đặc điểm của nó là thực thi mã trong ngữ cảnh coroutine bất đồng bộ gắn với vòng đời của phần tử composable. Không giống các hàm callback thông thường, LaunchedEffect không chặn UI và có thể thực hiện các thao tác dài như yêu cầu mạng hoặc chờ đợi.
Bên trong, LaunchedEffect sử dụng một CoroutineScope được cung cấp bởi composition. Phạm vi này tự động bị hủy khi phần tử composable rời khỏi composition. Sự ràng buộc này đảm bảo rằng không có coroutine nào tiếp tục chạy sau khi màn hình đóng — đây là sự khác biệt chính so với coroutine toàn cục trong phạm vi ViewModel hoặc Application.
Theo Android Developers Blog (2025), LaunchedEffect được thiết kế đặc biệt để thay thế mô hình LiveData-observer trong thế giới Compose. Thay vì đăng ký LiveData qua observeAsState và quản lý đăng ký riêng rẽ, các nhà phát triển sử dụng LaunchedEffect với collectAsState trên Flow, giúp quản lý vòng đời dễ đoán hơn và loại bỏ rò rỉ bộ nhớ vốn có trong các đăng ký không có hủy tường minh.
@Composable
fun UserProfileScreen(userId: Int) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
val result = userRepository.fetchUser(userId)
userData = result
}
// Giao diện dựa trên userData
}
Cơ chế quan trọng nhất của LaunchedEffect là hệ thống khóa. Tham số đầu tiên của hàm — vararg keys: Any? — xác định khi nào hiệu ứng nên khởi động lại. LaunchedEffect lưu trữ các giá trị khóa trước đó và so sánh chúng với các giá trị mới ở mỗi lần tái hợp thành. Nếu ít nhất một khóa đã thay đổi (qua equals()), coroutine hiện tại bị hủy và một coroutine mới được bắt đầu.
Nếu khóa là, ví dụ, userId, khi định danh người dùng thay đổi, LaunchedEffect sẽ tự động hủy yêu cầu hiện tại và bắt đầu yêu cầu mới với userId được cập nhật. Điều này giúp nhà phát triển không cần hủy thủ công yêu cầu trước đó và kiểm tra tính phù hợp của dữ liệu — mọi thứ được quản lý khai báo thông qua khóa. Cách tiếp cận này phù hợp với mô hình phản ứng của Jetpack Compose.
Quy tắc quan trọng: nếu bạn truyền một hằng số làm khóa — LaunchedEffect(Unit) — hiệu ứng sẽ chỉ chạy một lần khi vào composition, tương tự onStart hoặc onResume trong Android cổ điển. Nếu bạn không truyền khóa, hiệu ứng sẽ chạy một lần khi composition. Nếu bạn truyền dấu ngoặc rỗng, LaunchedEffect sẽ không biên dịch, vì khóa là tham số bắt buộc.
// Thực thi một lần khi màn hình xuất hiện
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// Khởi động lại khi userId thay đổi
LaunchedEffect(userId) {
loadUserData(userId)
}
// Nhiều khóa
LaunchedEffect(userId, filter, sortOrder) {
fetchFilteredData(userId, filter, sortOrder)
}
Mặc dù cả hai API đều thuộc hiệu ứng phụ trong Jetpack Compose, LaunchedEffect và DisposableEffect giải quyết các nhiệm vụ khác nhau về cơ bản. LaunchedEffect được thiết kế cho coroutine bất đồng bộ với khả năng khởi động lại bằng khóa, trong khi DisposableEffect dành cho các thao tác thiết lập và dọn dẹp đồng bộ không có coroutine.
Sự khác biệt chính là sự hiện diện của onDispose trong DisposableEffect. LaunchedEffect không có khối dọn dẹp tường minh: việc hủy coroutine xảy ra tự động khi khóa thay đổi hoặc rời khỏi composition, nhưng nhà phát triển không thể chèn mã tùy chỉnh vào thời điểm hủy. DisposableEffect, ngược lại, cung cấp một khối onDispose được đảm bảo thực thi khi rời khỏi composition, điều này rất quan trọng để giải phóng tài nguyên gốc.
| Đặc điểm | LaunchedEffect | DisposableEffect |
|---|---|---|
| Thực thi | Bất đồng bộ (coroutine) | Đồng bộ |
| onDispose | Không (tự động hủy coroutine) | Có (khối dọn dẹp tường minh) |
| Khóa | Khởi động lại + hủy coroutine cũ | Thực thi onDispose + khởi tạo lại |
| Sử dụng điển hình | Yêu cầu mạng, đăng ký Flow, bộ đếm thời gian | BroadcastReceiver, cảm biến, trình lắng nghe gốc |
| Hủy khi thoát | Tự động | Qua onDispose |
Theo bài viết của Google “Compose Side Effects: Deep Dive” (2025), việc chọn đúng giữa LaunchedEffect và DisposableEffect được xác định bởi loại tài nguyên: nếu thao tác là một coroutine có thể hủy — hãy sử dụng LaunchedEffect. Nếu tài nguyên yêu cầu gọi tường minh close(), unregister() hoặc dispose() — hãy sử dụng DisposableEffect.
Trường hợp sử dụng phổ biến nhất của LaunchedEffect là tải dữ liệu khi màn hình mở. Mô hình đơn giản: bên trong LaunchedEffect, một hàm suspend của repository hoặc UseCase được gọi, kết quả được gán cho một biến state và UI tự động vẽ lại. LaunchedEffect đảm bảo rằng khi màn hình được mở lại (ví dụ, khi điều hướng quay lại), việc tải được thực hiện lại nếu các khóa đã thay đổi.
Để hiển thị trạng thái tải, một mô hình ba trạng thái được sử dụng: Loading, Success, Error. LaunchedEffect được bọc trong try-catch, và khi thành công state = Success(data) được đặt, khi lỗi — state = Error(exception). UI phản ứng với trạng thái và hiển thị màn hình tương ứng: trình tải shimmer, dữ liệu hoặc màn hình lỗi với nút thử lại.
Nếu cần tải dữ liệu trong khi cuộn (phân trang), LaunchedEffect kết hợp với LazyColumn và LazyListState: khi đạt đến cuối danh sách, khóa LaunchedEffect được cập nhật (ví dụ, bộ đếm trang), kích hoạt tải phần tiếp theo của dữ liệu.
@Composable
fun ArticleScreen(articleId: Int) {
var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
LaunchedEffect(articleId) {
state = UiState.Loading
state = try {
UiState.Success(articleRepository.fetch(articleId))
} catch (e: Exception) {
UiState.Error(e)
}
}
when (val s = state) {
is UiState.Loading -> ShimmerPlaceholder()
is UiState.Success -> ArticleContent(s.data)
is UiState.Error -> ErrorScreen(s.error)
{ // onRetry callback (state updates) }
}
}
Việc sử dụng đúng các khóa của LaunchedEffect là chìa khóa để làm việc hiệu quả với các hiệu ứng. Nếu khóa là một giá trị có thể thay đổi thường xuyên (ví dụ, văn bản truy vấn tìm kiếm với mỗi lần nhập ký tự), mỗi ký tự sẽ hủy coroutine trước đó và bắt đầu một coroutine mới. Đối với tìm kiếm có debounce, điều này là quá mức — tốt hơn nên sử dụng debounce bên trong coroutine.
Để thực hiện debounce bên trong LaunchedEffect, hãy sử dụng delay() trước khi thực thi hành động chính. Ví dụ, khi tìm kiếm: LaunchedEffect(query) được kích hoạt ở mỗi thay đổi truy vấn, nhưng trước khi thực hiện yêu cầu có delay(500). Nếu người dùng nhập ký tự tiếp theo trước khi 500 ms trôi qua, coroutine bị hủy (do thay đổi khóa) và một coroutine mới được bắt đầu — do đó, yêu cầu chỉ được gửi sau khi tạm dừng 500 ms trong nhập liệu.
Một kỹ thuật khác là sử dụng sealed class làm khóa. Điều này cho phép kiểm soát chính xác khi nào hiệu ứng nên khởi động lại. Ví dụ, một khóa bao bọc chứa một định danh và cờ cập nhật bắt buộc: khi cờ thay đổi từ false thành true, LaunchedEffect khởi động lại ngay cả khi định danh không thay đổi. Mô hình này thuận tiện cho pull-to-refresh.
// Tìm kiếm với debounce 500ms
LaunchedEffect(searchQuery) {
delay(500)
searchResults.value = repository.search(searchQuery)
}
// Pull-to-refresh với cập nhật bắt buộc
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }
LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
articles = repository.loadUserArticles(userId)
}
Lỗi đầu tiên và phổ biến nhất là sử dụng LaunchedEffect không có khóa. Nếu bạn viết LaunchedEffect { ... } không có đối số, coroutine sẽ khởi động lại ở mỗi lần tái hợp thành, dẫn đến vòng lặp vô hạn các yêu cầu. LaunchedEffect yêu cầu ít nhất một khóa — thường là Unit để thực thi một lần.
Lỗi thứ hai là cố gắng sử dụng LaunchedEffect cho đăng ký Flow mà không có collect. Nếu bạn gọi collect trên Flow bên trong LaunchedEffect, coroutine sẽ bị treo cho đến khi Flow hoàn thành (điều mà trong trường hợp StateFlow không bao giờ xảy ra). Cách tiếp cận đúng là sử dụng collectLatest, giúp hủy bỏ lần thu thập trước khi có giá trị mới đến.
Lỗi thứ ba là truyền các đối tượng lồng nhau làm khóa. Nếu khóa là một data class với các trường có thể thay đổi (var), LaunchedEffect có thể không nhận ra sự thay đổi, vì Compose sử dụng equals() để so sánh, có thể hoạt động không ổn định với các trường var. Luôn sử dụng các đối tượng bất biến (val) hoặc nguyên thủy làm khóa LaunchedEffect.
Câu hỏi thường gặp
Nếu bạn không truyền khóa, LaunchedEffect sẽ không biên dịch — Kotlin yêu cầu ít nhất một đối số cho tham số vararg. Sử dụng LaunchedEffect(Unit) để thực thi một lần khi vào composition hoặc truyền các giá trị cụ thể mà khi thay đổi sẽ kích hoạt khởi động lại hiệu ứng.
Không, LaunchedEffect tự động hủy coroutine khi composable rời khỏi composition, ngăn ngừa rò rỉ bộ nhớ. Tuy nhiên, nếu coroutine bên trong LaunchedEffect giữ tham chiếu đến Activity hoặc Context thông qua closure, rò rỉ có thể xảy ra — hãy sử dụng viewModelScope cho các thao tác dài trong ViewModel.
LaunchedEffect tự động thực thi một coroutine khi vào composition với ràng buộc khóa. rememberCoroutineScope cung cấp phạm vi để chạy coroutine thủ công, ví dụ, để phản hồi onItemClick. Sử dụng LaunchedEffect cho các hiệu ứng phụ tự động và rememberCoroutineScope để chạy coroutine dựa trên sự kiện người dùng.
Nếu khóa của LaunchedEffect là một loại không ổn định (ví dụ, var hoặc một lớp không có equals()), Compose có thể không nhận ra rằng giá trị không thay đổi và sẽ khởi động lại hiệu ứng ở mỗi lần tái hợp thành. Giải pháp: sử dụng các loại ổn định (nguyên thủy, chuỗi, data class với trường val) hoặc bọc các giá trị có thể thay đổi trong remember.
Không có cách trực tiếp để dừng LaunchedEffect từ bên ngoài — quyền kiểm soát được quản lý thông qua khóa. Thay đổi khóa để hủy coroutine hiện tại. Nếu bạn cần kiểm soát hoàn toàn vòng đời coroutine, hãy sử dụng rememberCoroutineScope với Job và tự gọi job.cancel() khi có sự kiện hoặc thay đổi trạng thái.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm