SideEffect — nó là gì, đồng bộ hóa trạng thái trong Jetpack Compose

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

SideEffect là một hàm composable trong Jetpack Compose thực thi khối mã được truyền vào mỗi lần tái hợp thành thành công. Không giống như LaunchedEffect và DisposableEffect, SideEffect không gắn với khóa và không có khối dọn dẹp — nó chỉ đơn giản đồng bộ hóa trạng thái Compose với các hệ thống bên ngoài sau mỗi lần kết xuất. Điều này làm cho nó trở nên lý tưởng để cập nhật các hàm callback, đồng bộ hóa với ViewPager và truyền dữ liệu cho Analytics SDK. Theo Android Developers Documentation (2025), SideEffect được thực thi nghiêm ngặt sau khi Compose xác nhận tái hợp thành thành công và không được thực thi nếu tái hợp thành bị bỏ qua.

Điểm chính

  • SideEffect — API tác dụng phụ cho mã được thực thi sau mỗi lần tái hợp thành thành công.
  • Đồng bộ hóa — truyền trạng thái Compose đến các hệ thống bên ngoài không hỗ trợ Compose.
  • Không có khóa — không giống LaunchedEffect, SideEffect không khởi động lại mà thực thi ở mỗi lần tái hợp thành.
  • Không dọn dẹp — SideEffect không cung cấp onDispose, nó được thiết kế chỉ cho đồng bộ hóa một chiều.
  • Đồng bộ — khối được thực thi đồng bộ trong pha composition của Compose, không có coroutine.

SideEffect trong Jetpack Compose là gì

SideEffect là API tác dụng phụ đơn giản nhất trong Jetpack Compose. Nó thực thi một khối mã ở mỗi lần tái hợp thành thành công của một thành phần composable. Từ “thành công” là chìa khóa ở đây: nếu Compose quyết định rằng không cần tái hợp thành (ví dụ, tất cả các tham số đầu vào không thay đổi và kết quả sẽ giống nhau), SideEffect sẽ không được thực thi. Điều này đảm bảo rằng khối đồng bộ hóa chỉ được gọi khi giao diện người dùng thực sự thay đổi.

Trường hợp sử dụng chính của SideEffect là đồng bộ hóa trạng thái Compose với các đối tượng không phải là một phần của cây Compose. Các ví dụ điển hình bao gồm: cập nhật hàm callback trong hệ thống Legacy View, truyền trạng thái hiện tại cho ViewPager, gửi sự kiện đến SDK phân tích khi dữ liệu hiển thị thay đổi và đồng bộ hóa với SDK bản đồ mong đợi cập nhật ở định dạng bên ngoài.

Theo Android Developer Blog (2025), SideEffect thường được sử dụng kết hợp với remember: remember bảo quản một đối tượng (ví dụ, một callback) và SideEffect cập nhật nó bất cứ khi nào một phụ thuộc thay đổi. Mô hình này đặc biệt quan trọng đối với các thư viện chấp nhận đối tượng listener và không tạo lại chúng khi cập nhật — không có SideEffect, listener sẽ giữ một tham chiếu cũ đến trạng thái hiện tại.

kotlin
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setZoom(zoomLevel)
        mapView.updateMarkers(markers)
    }
    
    AndroidView(factory = { mapView })
}

Cách SideEffect hoạt động và các pha composition

Để hiểu SideEffect, bạn cần hiểu các pha thực thi của Jetpack Compose. Compose trải qua ba pha cho mỗi khung hình: Composition (hiển thị gì), Layout (hiển thị ở đâu), Drawing (hiển thị như thế nào). SideEffect được thực thi ở cuối pha Composition — sau khi tất cả các hàm composable đã chạy, nhưng trước pha Layout. Điều này đảm bảo rằng SideEffect nhìn thấy trạng thái cuối cùng của tất cả các biến sau khi tái hợp thành.

Vị trí này trong vòng đời mang lại một lợi thế quan trọng: SideEffect không thể gây ra tái hợp thành vô hạn, ngay cả khi trạng thái được thay đổi bên trong nó. Bởi vì nó được thực thi sau composition, các thay đổi được thực hiện bên trong SideEffect sẽ chỉ được áp dụng trong khung hình tiếp theo — điều này ngăn chặn các chu kỳ điển hình của việc thay đổi trạng thái bên trong thân hàm composable (khi setState trong composition kích hoạt một composition mới trước khi composition hiện tại kết thúc).

Một đặc điểm khác là SideEffect không được tối ưu hóa bởi khóa. Nó được thực thi ở mỗi lần tái hợp thành bất kể trạng thái cụ thể nào đã thay đổi. Nếu bạn cần kiểm soát chính xác hơn (chỉ thực thi khi một tham số cụ thể thay đổi), hãy sử dụng LaunchedEffect với khóa hoặc bọc SideEffect trong kiểm tra thay đổi thông qua remember.

kotlin
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }

SideEffect {
    if (currentZoom != zoomLevel) {
        map.animateToZoom(zoomLevel)
        currentZoom = zoomLevel
    }
}

// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change

Cập nhật hàm callback với SideEffect

Kịch bản thực tế phổ biến nhất cho SideEffect là cập nhật các hàm callback đang nắm bắt trạng thái hiện tại. Trong Jetpack Compose, điều này được gọi là “quản lý vòng đời callback”. Vấn đề là các biểu thức lambda trong Kotlin nắm bắt biến theo tham chiếu, và nếu một callback được tạo với một giá trị và sau đó biến thay đổi — callback tiếp tục sử dụng giá trị cũ.

Xét một ví dụ: Google Maps SDK cho Android chấp nhận một đối tượng OnCameraMoveListener thông qua setOnCameraMoveListener(). Nếu bạn truyền một lambda nắm bắt isTrackingEnabled, khi isTrackingEnabled thay đổi, lambda sẽ không được cập nhật — Maps SDK sẽ tiếp tục gọi callback cũ với dữ liệu lỗi thời. SideEffect giải quyết vấn đề này: nó thiết lập lại listener ở mỗi lần tái hợp thành, đảm bảo rằng SDK luôn sử dụng lambda hiện tại với trạng thái mới nhất.

Theo Tài liệu Maps SDK cho Android (2025), Google khuyến nghị chính xác mô hình này khi tích hợp Maps với Jetpack Compose. Một cách tiếp cận tương tự được sử dụng cho WebView, VideoView, TextureView và bất kỳ thành phần dựa trên View nào chấp nhận callback thông qua các phương thức set. SideEffect đảm bảo rằng các callback luôn được cập nhật với mỗi thay đổi trạng thái.

kotlin
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setOnMarkerClickListener { marker ->
            onMarkerClick(marker)
            true
        }
        mapView.isTrafficEnabled = isTrackingEnabled
    }
    
    AndroidView(factory = { mapView })
}

Đồng bộ hóa với Analytics SDK

Một kịch bản quan trọng khác cho SideEffect là gửi sự kiện đến các hệ thống phân tích khi trạng thái giao diện người dùng thay đổi. Ví dụ, khi người dùng chuyển tab trong TabLayout trong màn hình Compose, SideEffect có thể truyền trạng thái tab được chọn hiện tại đến Firebase Analytics hoặc AppsFlyer. Mỗi khi tab được chọn thay đổi (và tái hợp thành xảy ra), SideEffect gửi sự kiện tương ứng.

Sự khác biệt so với việc gửi sự kiện trực tiếp trong onClick hoặc onTabSelected là SideEffect được kích hoạt bởi các thay đổi trạng thái từ bất kỳ nguồn nào — không chỉ hành động của người dùng, mà còn các thay đổi lập trình, khôi phục trạng thái sau khi xoay màn hình hoặc Deep Links. Điều này làm cho SideEffect trở thành một cơ chế đồng bộ hóa phổ quát không phụ thuộc vào nguồn thay đổi.

Theo Firebase Best Practices (Google, 2025), việc gửi sự kiện phân tích qua SideEffect cung cấp một bức tranh đầy đủ hơn về hành trình người dùng, vì nó nắm bắt tất cả các thay đổi trạng thái, bao gồm cả những thay đổi xảy ra mà không có hành động trực tiếp của người dùng. Tuy nhiên, điều quan trọng là không lạm dụng: mỗi sự kiện phân tích là một yêu cầu mạng, vì vậy đối với các trạng thái thay đổi thường xuyên (vị trí cuộn, tọa độ ngón tay), SideEffect không phù hợp — hãy sử dụng debounce hoặc chỉ gửi sự kiện khi có thay đổi đáng kể.

kotlin
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
    val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
    
    SideEffect {
        val params = Bundle().apply {
            putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
            putString(FirebaseAnalytics.Param.ITEM_ID, productId)
        }
        firebaseAnalytics.logEvent(        FirebaseAnalytics.Event.VIEW_ITEM, params)
    }
    
    // UI with TabRow and selected tab
}

SideEffect vs LaunchedEffect: khi nào sử dụng cái gì

Sự lựa chọn giữa SideEffectLaunchedEffect phụ thuộc vào hai yếu tố: có cần thực thi bất đồng bộ không và có cần điều khiển dựa trên khóa không. SideEffect là đồng bộ và được thực thi ở mỗi lần tái hợp thành. LaunchedEffect là bất đồng bộ (coroutine) và chỉ được thực thi khi một khóa thay đổi, không phải ở mỗi lần tái hợp thành.

Đặc điểmSideEffectLaunchedEffect
Thực thiỞ mỗi lần tái hợp thànhKhi khóa thay đổi
Bất đồng bộĐồng bộCoroutine
KhóaKhôngCó (vararg)
Dọn dẹpKhôngTự động hủy coroutine
Sử dụng điển hìnhCallback, phân tích, đồng bộ hóa viewTải dữ liệu, đăng ký Flow, bộ đếm thời gian

Trong thực tế, 70% trường hợp sử dụng tác dụng phụ được bao phủ bởi LaunchedEffect (các thao tác bất đồng bộ, tải dữ liệu), 20% bởi DisposableEffect (tài nguyên có dọn dẹp) và chỉ 10% bởi SideEffect (đồng bộ hóa callback). SideEffect là một công cụ chuyên dụng cho một phạm vi nhiệm vụ hạn chế, nhưng trong những nhiệm vụ đó, nó không thể thiếu.

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

Lỗi chính là thay đổi trạng thái Compose bên trong SideEffect. Mặc dù SideEffect không trực tiếp gây ra vòng lặp vô hạn (vì nó được thực thi sau pha composition), nó có thể kích hoạt các lần tái hợp thành quá mức. Nếu trạng thái được thay đổi bên trong SideEffect (mutableStateOf), nó sẽ kích hoạt một lần tái hợp thành mới trong khung hình tiếp theo, lần này lại thực thi SideEffect — và cứ tiếp tục cho đến khi ổn định. Đây không phải là vòng lặp vô hạn, nhưng là công việc không cần thiết cho framework.

Lỗi thứ hai là thực hiện các tính toán nặng bên trong SideEffect. Vì SideEffect được gọi ở mỗi lần tái hợp thành và các lần tái hợp thành có thể xảy ra hàng chục lần mỗi giây (trong quá trình hoạt hình, cuộn), bất kỳ mã nặng nào bên trong SideEffect sẽ dẫn đến rụt khung hình. Di chuyển các thao tác nặng ra khỏi composition — vào một coroutine (LaunchedEffect) hoặc tính toán qua derivedStateOf / remember.

Lỗi thứ ba là cố gắng sử dụng SideEffect cho mã bất đồng bộ. SideEffect không phải là hàm suspend, do đó delay(), await(), collect() và các thao tác coroutine khác bên trong nó sẽ không biên dịch. Nếu bạn cần thực hiện một hành động bất đồng bộ sau khi tái hợp thành, hãy sử dụng snapshotFlow { ... } kết hợp với LaunchedEffect hoặc khởi chạy một coroutine qua rememberCoroutineScope.

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

SideEffect có được thực thi ở lần composition đầu tiên không?

Có, SideEffect được thực thi ở mỗi lần composition thành công, bao gồm cả lần đầu tiên — khi thành phần lần đầu xuất hiện trên màn hình. Điều này khác với LaunchedEffect(Unit), cũng được thực thi một lần ở lần composition đầu tiên nhưng không được thực thi ở các lần tái hợp thành sau (nếu khóa không thay đổi).

SideEffect có thể gây ra vòng lặp vô hạn không?

Không, SideEffect được thực thi sau pha composition — các thay đổi được thực hiện bên trong nó sẽ chỉ được áp dụng trong khung hình tiếp theo, ngăn chặn các vòng lặp. Tuy nhiên, thay đổi trạng thái thường xuyên bên trong SideEffect có thể gây ra một loạt các lần tái hợp thành, làm giảm hiệu suất. Chỉ thay đổi trạng thái bên trong SideEffect khi thực sự cần thiết.

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

SideEffect được thực thi đồng bộ ở mỗi lần tái hợp thành. snapshotFlow tạo một Flow từ trạng thái Compose và có thể được sử dụng với collectLatest trong LaunchedEffect để xử lý thay đổi theo phản ứng. snapshotFlow phù hợp cho các trường hợp bạn cần phản ứng với các thay đổi bằng debounce, filter hoặc distinctUntilChanged — điều không thể thực hiện được trong SideEffect đồng bộ.

Làm thế nào để gỡ lỗi SideEffect nếu nó được thực thi quá thường xuyên?

Sử dụng Android Studio Compose Modifier Debugger hoặc thêm ghi log với tên thành phần và tần suất gọi. Nếu SideEffect được thực thi thường xuyên hơn dự kiến, hãy kiểm tra xem trạng thái của thành phần cha có thay đổi không cần thiết không. Tối ưu hóa: tách các phần ổn định của giao diện người dùng vào các hàm composable riêng biệt với các chú thích unstable để giảm số lần tái hợp thành.

Có thể kết hợp SideEffect với DisposableEffect không?

Có, chúng có thể được sử dụng trong cùng một thành phần cho các mục đích khác nhau. DisposableEffect chịu trách nhiệm thiết lập và dọn dẹp một tài nguyên (một lần), trong khi SideEffect xử lý việc đồng bộ hóa trạng thái hiện tại với tài nguyên đó ở mỗi lần tái hợp thành. Một ví dụ điển hình: DisposableEffect đăng ký một callback qua API và SideEffect cập nhật dữ liệu được nắm bắt trong callback đó ở mỗi lần thay đổi.

Tổng kết

  • SideEffect — API tác dụng phụ cho mã đồng bộ được thực thi sau mỗi lần tái hợp thành thành công trong Jetpack Compose.
  • Đồng bộ hóa — trường hợp sử dụng chính: truyền trạng thái Compose đến các hệ thống bên ngoài (Google Maps, WebView, ViewPager, Analytics SDK).
  • Callback — SideEffect đảm bảo rằng các hàm callback nắm bắt trạng thái hiện tại luôn được cập nhật với mỗi lần cập nhật giao diện.
  • Không có vòng lặp — được thực thi sau pha composition, do đó việc thay đổi trạng thái bên trong SideEffect không gây ra tái hợp thành vô hạn.
  • Giới hạn — không hỗ trợ khóa, bất đồng bộ hoặc khối dọn dẹp; cho các nhiệm vụ này, hãy sử dụng LaunchedEffect hoặc DisposableEffect.
  • Hiệu suất — tránh các tính toán nặng bên trong SideEffect, vì nó được thực thi ở mỗi lần tái hợp thành (lên đến 60 lần mỗi giây).
  • Gỡ lỗi — kiểm soát tần suất gọi qua Compose Debugger và tối ưu hóa với remember để lọc các lần tái hợp thành không cần thiế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.

Thảo luận dự án

Đọc thêm