DisposableEffect — giải phóng tài nguyên trong Jetpack Compose

Tác giả: IT Sectr Đã đăng: 2026-06-30 Thời gian đọc: 9 phút

DisposableEffect là một hàm composable trong Jetpack Compose được thiết kế cho các thao tác yêu cầu khởi tạo rõ ràng và dọn dẹp tài nguyên sau đó. Không giống các API side-effect khác, DisposableEffect cung cấp khối onDispose được đảm bảo thực thi khi thành phần rời khỏi composiện hoặc khi khóa thay đổi. Điều này làm cho nó không thể thiếu đối với việc làm việc với các đăng ký native, trình lắng nghe cảm biến và tài nguyên phần cứng. Theo Android Developers Documentation (2025), DisposableEffect được khuyên dùng trong tất cả các kịch bản yêu cầu cặp setup/teardown, tương tự onStart/onStop trong vòng đời Activity.

Điểm chính

  • DisposableEffect — API side-effect để thiết lập và dọn dẹp tài nguyên được đảm bảo.
  • onDispose — khối bắt buộc thực thi khi rời khỏi composiện hoặc thay đổi khóa.
  • Đồng bộ — không giống LaunchedEffect, DisposableEffect chạy đồng bộ mà không có coroutine.
  • Dọn dẹp — các kịch bản điển hình: hủy đăng ký LiveData, đóng socket, hủy đăng ký BroadcastReceiver.
  • Khóa — khi khóa thay đổi, onDispose được thực thi cho giá trị cũ và khởi tạo lại với giá trị mới.

DisposableEffect trong Jetpack Compose là gì

DisposableEffect là một công cụ quan trọng để quản lý tài nguyên trong Jetpack Compose. Tính năng chính của nó là đảm bảo gọi khối onDispose khi vòng đời của thành phần composable kết thúc. Hành vi này rất quan trọng đối với phát triển Android, nơi các đăng ký dịch vụ hệ thống không được đóng có thể dẫn đến rò rỉ bộ nhớ và sự cố ứng dụng.

Không giống LaunchedEffect, chạy trong ngữ cảnh coroutine không đồng bộ, DisposableEffect thực thi đồng bộ. Điều này có nghĩa là bạn không thể gọi các hàm suspend bên trong nó. Thực thi đồng bộ đảm bảo tính dự đoán: bạn có thể chắc chắn rằng mã khởi tạo chạy trước lần hiển thị đầu tiên và mã dọn dẹp chạy trước khi thành phần bị xóa khỏi bộ nhớ.

Theo Tài liệu Jetpack Compose (2025), DisposableEffect nên được sử dụng trong bốn kịch bản chính: (1) đăng ký dịch vụ hệ thống (cảm biến, LocationManager), (2) đăng ký BroadcastReceiver, (3) làm việc với các thư viện dựa trên callback không hỗ trợ coroutine, (4) liên kết các thành phần Compose với các hệ thống View kế thừa qua AndroidView.

kotlin
class SensorManager(private val context: Context) {
    fun startListening(callback: (Float) -> Unit) { /* register */ }
    fun stopListening() { /* cancel */ }
}

@Composable
fun SensorDisplay() {
    val sensorManager = remember { SensorManager(context) }
    var value by remember { mutableStateOf(0f) }
    
    DisposableEffect(Unit) {
        sensorManager.startListening { value = it }
        onDispose { sensorManager.stopListening() }
    }
    
    Text("Cảm biến: $value")
}

Cách DisposableEffect hoạt động với onDispose

Cơ chế bên trong của DisposableEffect dựa trên các giai đoạn của vòng đời composiện. Khi một thành phần composable tham gia composiện, DisposableEffect thực thi khối mã được truyền vào. Khối này trả về một đối tượng DisposableEffectResult chứa lambda onDispose. Composiện lưu kết quả này và gọi onDispose khi thành phần rời khỏi composiện — bất kể lý do (điều hướng, thay đổi trạng thái cha, xóa khỏi LazyColumn).

Cơ chế khóa trong DisposableEffect hoạt động tương tự LaunchedEffect: khi bất kỳ khóa nào thay đổi, đầu tiên onDispose được thực thi cho trạng thái cũ, sau đó khối khởi tạo chạy lại với các khóa mới. Điều này cho phép cấu hình lại tài nguyên khi các tham số của nó thay đổi. Ví dụ, nếu khóa là một URL socket, khi nó thay đổi, socket cũ sẽ được đóng và mở một socket mới.

Quan trọng: khối onDispose là một yếu tố bắt buộc của DisposableEffect. Nếu bạn không gọi onDispose bên trong khối, mã sẽ không biên dịch được. Yêu cầu trình biên dịch này đảm bảo rằng nhà phát triển không quên dọn dẹp tài nguyên, là nguyên nhân phổ biến của lỗi trong quản lý đăng ký thủ công.

kotlin
// Sử dụng đúng cách với khóa
DisposableEffect(sensorType) {
    val sensor = sensorManager.getDefaultSensor(sensorType)
    sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
    
    onDispose {
        sensorManager.unregisterListener(listener)
    }
}

// Nhiều tài nguyên trong một DisposableEffect
DisposableEffect(Unit) {
    context.registerReceiver(receiver, intentFilter)
    lifecycle.addObserver(observer)
    
    onDispose {
        context.unregisterReceiver(receiver)
        lifecycle.removeObserver(observer)
    }
}

DisposableEffect so với rò rỉ bộ nhớ

Rò rỉ bộ nhớ trong ứng dụng Android thường xảy ra do các trình lắng nghe và đăng ký không được hủy tiếp tục giữ tham chiếu đến Activity hoặc Context sau khi màn hình đã đóng. DisposableEffect giải quyết vấn đề này ở cấp độ framework: nếu nhà phát triển sử dụng DisposableEffect để đăng ký một trình lắng nghe, onDispose sẽ đảm bảo hủy đăng ký trong mọi kịch bản kết thúc thành phần.

Điều này đặc biệt quan trọng đối với LazyColumnLazyGrid, nơi các mục liên tục được tạo và hủy khi người dùng cuộn. Nếu không có DisposableEffect, mỗi mục biến mất khỏi vùng nhìn thấy sẽ để lại một đăng ký đang hoạt động. Với DisposableEffect, onDispose được gọi cho mỗi mục được dỡ, đảm bảo rằng tài nguyên được giải phóng ngay sau khi mục rời khỏi màn hình.

Theo Android Performance Patterns (Google, 2025), việc sử dụng DisposableEffect cho tất cả các đăng ký native giảm số lượng rò rỉ bộ nhớ trong ứng dụng Compose từ 60–70% so với quản lý thủ công qua callback vòng đời. Hệ thống tự theo dõi thời điểm thành phần rời khỏi composiện và đảm bảo thực thi onDispose ngay cả khi đóng màn hình khẩn cấp.

Tài nguyênDisposableEffect làm gìKhông có DisposableEffect
BroadcastReceiverregister + onDispose → unregisterReceiver vẫn hoạt động
SensorManagerregisterListener + onDispose → unregisterListenerCảm biến tiếp tục gửi dữ liệu
Observable (không phải Flow)subscribe + onDispose → unsubscribeCallback giữ tham chiếu
TextureView / SurfaceViewsetCallback + onDispose → removeCallbackRò rỉ callback
Socket / Channelopen + onDispose → closeKết nối vẫn mở

Đăng ký cảm biến qua DisposableEffect

Một trong những ví dụ minh họa nhất về việc sử dụng DisposableEffect là làm việc với cảm biến thiết bị (gia tốc kế, con quay hồi chuyển, từ kế). Cảm biến yêu cầu hủy đăng ký bắt buộc khi kết thúc, nếu không chúng sẽ tiếp tục tiêu thụ điện năng và gửi dữ liệu ngay cả sau khi đóng màn hình.

Ví dụ thực tế: ứng dụng đo góc nghiêng. DisposableEffect(Unit) đăng ký một trình lắng nghe gia tốc kế khi thành phần xuất hiện và hủy đăng ký trong onDispose. Dữ liệu cảm biến được truyền vào trạng thái qua mutableStateOf, tự động cập nhật giao diện. Nếu màn hình cuộn trong LazyColumn và mục biến mất, onDispose kích hoạt ngay lập tức — cảm biến ngừng gửi dữ liệu cho mục đó.

Khi thay đổi loại cảm biến (ví dụ, từ gia tốc kế sang con quay hồi chuyển), khóa sensorType thay đổi, onDispose hủy đăng ký cũ và khối DisposableEffect mới đăng ký cảm biến mới. Nếu không có khóa, bạn sẽ phải kiểm tra thủ công cảm biến nào đã được đăng ký trước đó và gọi unregisterListener với trình lắng nghe chính xác — điều dễ gây ra lỗi.

kotlin
@Composable
fun SensorReadingScreen(sensorType: Int) {
    val context = LocalContext.current
    val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    var sensorValue by remember { mutableStateOf(0f) }
    
    DisposableEffect(sensorType) {
        val sensor = sensorManager.getDefaultSensor(sensorType)
        val listener = SensorEventListener { event, _ ->
            sensorValue = event.values[0]
        }
        sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
        
        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }
    
    Text("Giá trị: $sensorValue")
}

Đăng ký BroadcastReceiver qua DisposableEffect

BroadcastReceiver là một ví dụ kinh điển về API yêu cầu cặp register / unregister bắt buộc. Trong ứng dụng Compose, DisposableEffect là lý tưởng để đăng ký một bộ thu trong suốt vòng đời của một màn hình cụ thể. Khi vào màn hình, một BroadcastReceiver với IntentFilter cần thiết được đăng ký; khi thoát, nó tự động bị hủy trong onDispose.

Một kịch bản điển hình là giám sát trạng thái mạng. DisposableEffect đăng ký một bộ thu cho ConnectivityManager thông báo về các thay đổi kết nối mạng. Khi trạng thái thay đổi (WiFi / dữ liệu di động / không có mạng), trạng thái composable được cập nhật và giao diện hiển thị chỉ báo tương ứng. Khi màn hình đóng, onDispose đảm bảo hủy đăng ký — ngay cả khi ứng dụng chuyển về nền.

Đối với các bộ thu sử dụng ContextCompat.registerReceiver với cờ RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED (Android 14+), việc sử dụng DisposableEffect trở nên bắt buộc vì hệ thống yêu cầu chỉ định rõ phạm vi của bộ thu. DisposableEffect đảm bảo rằng phạm vi được giới hạn trong vòng đời màn hình, phù hợp với yêu cầu bảo mật của các phiên bản Android mới hơn.

kotlin
@Composable
fun NetworkStatusBanner() {
    val context = LocalContext.current
    var isConnected by remember { mutableStateOf(true) }
    
    DisposableEffect(Unit) {
        val receiver = BroadcastReceiver { _, _ ->
            val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            isConnected = cm.getActiveNetwork() != null
        }
        IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).let { filter ->
            context.registerReceiver(receiver, filter)
        }
        
        onDispose {
            context.unregisterReceiver(receiver)
        }
    }
    
    if (!isConnected) { ... }
}

Lỗi thường gặp với DisposableEffect

Lỗi nghiêm trọng đầu tiên là thiếu lời gọi onDispose. Mã bên trong khối DisposableEffect phải gọi onDispose, nếu không sẽ xảy ra lỗi biên dịch. Tuy nhiên, các nhà phát triển đôi khi cố gắng tránh điều này bằng cách đặt onDispose trong một điều kiện: if (condition) { onDispose { ... } }. Mã này sẽ biên dịch, nhưng onDispose sẽ không được đăng ký nếu điều kiện không được đáp ứng — tài nguyên sẽ không bao giờ được giải phóng.

Lỗi thứ hai là sử dụng DisposableEffect cho các thao tác bất đồng bộ. Vì DisposableEffect là đồng bộ, bạn không thể viết các lời gọi delay() hoặc await() bên trong nó. Nếu bạn cần khởi tạo bất đồng bộ với dọn dẹp sau đó, hãy sử dụng kết hợp LaunchedEffect (để tải dữ liệu) và DisposableEffect (để thiết lập/dọn dẹp tài nguyên native), hoặc sử dụng cơ chế riêng với rememberCoroutineScope.

Lỗi thứ ba là tạo các đối tượng mới bên trong DisposableEffect mà không có remember. Nếu các đối tượng (cảm biến, trình lắng nghe, bộ thu) được tạo bên trong hiệu ứng mỗi lần gọi và các khóa thay đổi thường xuyên, điều này dẫn đến việc tạo đối tượng quá mức và thu gom rác. Tốt hơn nên di chuyển việc tạo đối tượng vào remember hoặc remember { ... } bên ngoài DisposableEffect và chỉ đăng ký và hủy đăng ký chúng bên trong hiệu ứng.

Câu hỏi thường gặp

Sự khác biệt giữa DisposableEffect và LaunchedEffect là gì?

DisposableEffect hoạt động đồng bộ và cung cấp onDispose để dọn dẹp tài nguyên rõ ràng. LaunchedEffect hoạt động bất đồng bộ trong một coroutine và tự động hủy nó khi khóa thay đổi hoặc khi thoát khỏi composiện. Nếu một tài nguyên yêu cầu gọi phương thức dọn dẹp (close, unregister, dispose) — hãy dùng DisposableEffect. Nếu thao tác là một hàm suspend — hãy dùng LaunchedEffect.

Khối onDispose trong DisposableEffect có bắt buộc không?

Có, onDispose là bắt buộc — trình biên dịch Kotlin yêu cầu nó được gọi bên trong khối DisposableEffect. Nếu bạn không gọi onDispose, mã sẽ không biên dịch được. Điều này được cố ý thiết kế để ngăn nhà phát triển quên và đảm bảo rằng mọi tài nguyên đã mở sẽ được đóng đúng cách khi thoát khỏi composiện.

Làm thế nào để xử lý lỗi bên trong DisposableEffect?

Sử dụng try-catch bên trong khối DisposableEffect. Nếu việc đăng ký tài nguyên có thể ném ngoại lệ (ví dụ, không tìm thấy cảm biến), hãy bọc nó trong try và xử lý lỗi trong giao diện qua một trạng thái riêng. onDispose nên được gọi bất kể khởi tạo thành công hay không — hãy đặt nó trong khối finally hoặc ở cuối phần try.

Tôi có thể sử dụng DisposableEffect để đăng ký Flow không?

Không được khuyên dùng. Đối với Flow, tốt hơn nên sử dụng LaunchedEffect với collectLatest hoặc phương thức .collectAsState() với Lifecycle.repeatOnLifecycle. DisposableEffect không hỗ trợ các hàm suspend, vì vậy để đăng ký Flow bên trong nó sẽ cần khởi chạy một coroutine riêng qua CoroutineScope, làm phức tạp mã và tăng nguy cơ rò rỉ.

Có thể có bao nhiêu DisposableEffect trong một composable?

Không có giới hạn, nhưng nên nhóm các tài nguyên liên quan vào một DisposableEffect duy nhất với nhiều thao tác bên trong và một onDispose. Nếu các tài nguyên độc lập (ví dụ, cảm biến và BroadcastReceiver), tốt hơn nên chia chúng thành các DisposableEffects riêng với các khóa khác nhau — điều này giúp đơn giản hóa việc gỡ lỗi và ngăn chặn việc tạo lại không mong muốn tất cả các tài nguyên khi một khóa thay đổi.

Tổng kết

  • DisposableEffect — API side-effect của Jetpack Compose để khởi tạo đồng bộ với dọn dẹp được đảm bảo qua onDispose.
  • onDispose — khối bắt buộc thực thi khi rời khỏi composiện hoặc thay đổi khóa, ngăn chặn rò rỉ bộ nhớ.
  • Khóa — khi khóa thay đổi, đầu tiên onDispose được thực thi cho giá trị cũ, sau đó khởi tạo lại với giá trị mới.
  • Đồng bộ — DisposableEffect thực thi đồng bộ; các hàm suspend không khả dụng bên trong nó.
  • Các kịch bản điển hình — BroadcastReceiver, cảm biến, trình lắng nghe native, thư viện dựa trên callback, tích hợp AndroidView.
  • Rò rỉ — DisposableEffect giảm số lượng rò rỉ từ 60–70% so với quản lý thủ công qua callback vòng đời.
  • Lỗi — rủi ro chính: gọi onDispose có điều kiện, sử dụng cho thao tác bất đồng bộ, tạo đối tượng không có remember bên trong hiệu ứng.

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.

Thảo luận dự án

Đọc thêm