Trong Jetpack Compose, các hàm có khả năng kết hợp thường giữ trạng thái bằng hàm remember. Bạn có thể sử dụng lại các giá trị được ghi nhớ trên các lần kết hợp lại, như
giải thích trong Trạng thái và Jetpack Compose.
Mặc dù remember đóng vai trò là một công cụ để duy trì các giá trị trên các lần kết hợp lại, nhưng trạng thái thường cần tồn tại lâu hơn vòng đời của một thành phần. Trang này giải thích sự
khác biệt giữa các API remember, retain, rememberSaveable,
và rememberSerializable, thời điểm chọn API nào và các
phương pháp hay nhất để quản lý các giá trị được ghi nhớ và giữ lại trong Compose.
Chọn vòng đời chính xác
Trong Compose, có một số hàm mà bạn có thể dùng để duy trì trạng thái trên các
thành phần và lâu hơn: remember, retain, rememberSaveable và
rememberSerializable. Các hàm này khác nhau về vòng đời và ngữ nghĩa, đồng thời mỗi hàm phù hợp để lưu trữ các loại trạng thái cụ thể. Các điểm khác biệt được nêu trong bảng sau:
|
|
|
|
|---|---|---|---|
Các giá trị vẫn tồn tại sau các lần kết hợp lại? |
✅ |
✅ |
✅ |
Các giá trị vẫn tồn tại sau các lần tạo lại hoạt động? |
❌ |
✅ Thực thể giống nhau ( |
✅ Một đối tượng tương đương ( |
Các giá trị vẫn tồn tại sau khi bị buộc tắt? |
❌ |
❌ |
✅ |
Loại dữ liệu được hỗ trợ |
Tất cả |
Không được tham chiếu đến bất kỳ đối tượng nào sẽ bị rò rỉ nếu hoạt động bị huỷ |
Phải có thể tuần tự hoá |
Trường hợp sử dụng |
|
|
|
remember
remember là cách phổ biến nhất để lưu trữ trạng thái trong Compose. Khi remember được
gọi lần đầu tiên, phép tính đã cho sẽ được thực thi và được
ghi nhớ, nghĩa là được Compose lưu trữ để thành phần kết hợp sử dụng lại trong tương lai. Khi một thành phần kết hợp kết hợp lại, thành phần đó sẽ thực thi lại mã của mình, nhưng mọi lệnh gọi đến remember đều trả về các giá trị từ thành phần trước đó thay vì thực thi lại phép tính.
Mỗi thực thể của một hàm composable có một tập hợp các giá trị được ghi nhớ riêng, được gọi là ghi nhớ theo vị trí. Khi các giá trị được ghi nhớ được ghi nhớ để sử dụng trên các lần kết hợp lại, chúng sẽ được liên kết với vị trí của chúng trong hệ phân cấp thành phần. Nếu một thành phần kết hợp được dùng ở nhiều vị trí, thì mỗi thực thể trong hệ phân cấp thành phần sẽ có một tập hợp các giá trị được ghi nhớ riêng.
Khi một giá trị được ghi nhớ không còn được sử dụng nữa, giá trị đó sẽ bị quên và bản ghi của giá trị đó sẽ bị loại bỏ. Các giá trị được ghi nhớ sẽ bị quên khi bị xoá khỏi hệ phân cấp thành phần (bao gồm cả khi một giá trị bị xoá và thêm lại để chuyển đến một vị trí khác mà không dùng thành phần kết hợp key hoặc MovableContent), hoặc được gọi bằng các tham số key khác nhau.
Trong số các lựa chọn hiện có, remember có vòng đời ngắn nhất và quên các giá trị sớm nhất trong 4 hàm ghi nhớ được mô tả trên trang này.
Điều này khiến hàm này phù hợp nhất với:
- Tạo các đối tượng trạng thái nội bộ, chẳng hạn như vị trí cuộn hoặc trạng thái ảnh động
- Tránh việc tạo lại đối tượng tốn kém trên mỗi lần kết hợp lại
Tuy nhiên, bạn nên tránh:
- Lưu trữ mọi hoạt động đầu vào của người dùng bằng
remember, vì các đối tượng được ghi nhớ sẽ bị quên trên các thay đổi về cấu hình của Hoạt động và sự kiện bị buộc tắt do hệ thống gây ra.
rememberSaveable và rememberSerializable
rememberSaveable và rememberSerializable được xây dựng dựa trên remember. Chúng có vòng đời dài nhất trong các hàm ghi nhớ được thảo luận trong hướng dẫn này.
Ngoài việc ghi nhớ các đối tượng theo vị trí trên các lần kết hợp lại, hàm này cũng có thể lưu các giá trị để có thể khôi phục các giá trị đó trên các lần tạo lại hoạt động, bao gồm cả các thay đổi về cấu hình và sự kiện bị buộc tắt (khi hệ thống buộc tắt quy trình của ứng dụng khi ứng dụng ở chế độ nền, thường là để giải phóng bộ nhớ cho các ứng dụng ở nền trước hoặc nếu người dùng thu hồi quyền của ứng dụng khi ứng dụng đang chạy).
rememberSerializable hoạt động giống như rememberSaveable, nhưng tự động hỗ trợ việc duy trì các loại phức tạp có thể tuần tự hoá bằng thư viện kotlinx.serialization. Chọn rememberSerializable nếu loại của bạn được đánh dấu (hoặc có thể được đánh dấu) bằng @Serializable và rememberSaveable trong tất cả các trường hợp khác.
Điều này khiến cả rememberSaveable và rememberSerializable trở thành ứng cử viên hoàn hảo để lưu trữ trạng thái liên kết với hoạt động đầu vào của người dùng, bao gồm cả mục nhập trường văn bản, vị trí cuộn, trạng thái bật/tắt, v.v. Bạn nên lưu trạng thái này để đảm bảo người dùng không bao giờ bị mất vị trí. Nhìn chung, bạn nên sử dụng rememberSaveable hoặc rememberSerializable để ghi nhớ mọi trạng thái mà ứng dụng của bạn không thể truy xuất từ một nguồn dữ liệu liên tục khác, chẳng hạn như cơ sở dữ liệu.
Xin lưu ý rằng rememberSaveable và rememberSerializable lưu các giá trị được ghi nhớ bằng cách tuần tự hoá các giá trị đó thành Bundle. Điều này có 2 hệ quả:
- Các giá trị bạn ghi nhớ phải có thể biểu thị bằng một hoặc nhiều loại dữ liệu sau: Nguyên thuỷ (bao gồm
Int,Long,Float,Double),Stringhoặc mảng của bất kỳ loại nào trong số này. - Khi một giá trị đã lưu được khôi phục, đó sẽ là một thực thể mới bằng
(
==) nhưng không phải là cùng một tham chiếu (===) mà thành phần đã dùng trước đó.
Để lưu trữ các loại dữ liệu phức tạp hơn mà không dùng kotlinx.serialization, bạn có thể triển khai Saver tuỳ chỉnh để tuần tự hoá và giải tuần tự hoá đối tượng của mình thành các loại dữ liệu được hỗ trợ. Xin lưu ý rằng Compose hiểu các loại dữ liệu phổ biến như State, List, Map, Set, v.v. ngay từ đầu và tự động chuyển đổi các loại này thành các loại được hỗ trợ thay cho bạn. Sau đây là ví dụ về Saver cho lớp Size. Hàm này được triển khai bằng cách đóng gói tất cả các thuộc tính của Size vào một danh sách bằng listSaver.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
API retain tồn tại giữa remember và
rememberSaveable/rememberSerializable về thời gian ghi nhớ các
giá trị. Hàm này được đặt tên khác vì các giá trị được giữ lại cũng có vòng đời khác với các giá trị được ghi nhớ tương ứng.
Khi một giá trị được giữ lại, giá trị đó vừa được ghi nhớ theo vị trí vừa được lưu trong một
cấu trúc dữ liệu thứ cấp có vòng đời riêng được liên kết với vòng đời của ứng dụng. Một giá trị được giữ lại có thể tồn tại sau các thay đổi về cấu hình mà không cần được tuần tự hoá, nhưng không thể tồn tại sau khi bị buộc tắt. Nếu một giá trị không được sử dụng sau khi hệ phân cấp thành phần được tạo lại, thì giá trị được giữ lại sẽ bị loại bỏ (tương đương với việc bị quên của retain).
Để đổi lấy vòng đời ngắn hơn rememberSaveable, retain có thể duy trì các giá trị không thể tuần tự hoá, chẳng hạn như biểu thức lambda, luồng và các đối tượng lớn như bitmap. Ví dụ: bạn có thể dùng retain để quản lý trình phát đa phương tiện (chẳng hạn như ExoPlayer) nhằm ngăn chặn việc phát lại đa phương tiện bị gián đoạn trong quá trình thay đổi cấu hình.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain so với ViewModel
Về cơ bản, cả retain và ViewModel đều cung cấp chức năng tương tự trong khả năng thường dùng nhất để duy trì các thực thể đối tượng trên các thay đổi về cấu hình. Việc chọn sử dụng retain hay ViewModel phụ thuộc vào loại giá trị bạn đang duy trì, cách phân phạm vi giá trị đó và liệu bạn có cần thêm chức năng hay không.
ViewModellà các đối tượng thường đóng gói hoạt động giao tiếp giữa giao diện người dùng và các lớp dữ liệu của ứng dụng. Các đối tượng này cho phép bạn di chuyển logic ra khỏi các hàm có khả năng kết hợp, giúp cải thiện khả năng kiểm thử. ViewModel được quản lý dưới dạng các singleton
trong ViewModelStore và có vòng đời khác với các giá trị được giữ lại. Mặc dù ViewModel sẽ vẫn hoạt động cho đến khi ViewModelStore bị
huỷ, nhưng các giá trị được giữ lại sẽ bị loại bỏ khi nội dung bị xoá vĩnh viễn
khỏi thành phần (ví dụ: đối với thay đổi về cấu hình, điều này có nghĩa là
giá trị được giữ lại sẽ bị loại bỏ nếu hệ phân cấp giao diện người dùng được tạo lại và giá trị được giữ lại
không được sử dụng sau khi thành phần được tạo lại).
ViewModel cũng bao gồm các tính năng tích hợp ngay từ đầu để chèn phần phụ thuộc bằng Dagger và Hilt, tích hợp với SavedState và hỗ trợ coroutine tích hợp để chạy các tác vụ ở chế độ nền. Điều này khiến ViewModel trở thành nơi lý tưởng để chạy các tác vụ ở chế độ nền và yêu cầu mạng, tương tác với các nguồn dữ liệu khác trong dự án của bạn, đồng thời tuỳ ý thu thập và duy trì trạng thái giao diện người dùng quan trọng đối với nhiệm vụ. Trạng thái này vừa phải được giữ lại trên các thay đổi về cấu hình trong ViewModel vừa phải tồn tại sau khi bị buộc tắt.
retain phù hợp nhất với các đối tượng được phân phạm vi cho các thực thể thành phần kết hợp cụ thể và không yêu cầu sử dụng lại hoặc chia sẻ giữa các thành phần kết hợp ngang hàng. Trong đó, ViewModel đóng vai trò là nơi phù hợp để lưu trữ trạng thái giao diện người dùng và thực hiện các tác vụ ở chế độ nền, retain là ứng cử viên phù hợp để lưu trữ các đối tượng cho cơ chế giao diện người dùng như bộ nhớ đệm, theo dõi và phân tích lần hiển thị, các phần phụ thuộc vào AndroidView và các đối tượng khác tương tác với Hệ điều hành Android hoặc quản lý các thư viện bên thứ ba như công ty xử lý thanh toán hoặc quảng cáo.
Đối với người dùng nâng cao thiết kế các mẫu kiến trúc ứng dụng tuỳ chỉnh bên ngoài các đề xuất về kiến trúc ứng dụng Android hiện đại: bạn cũng có thể dùng retain để xây dựng API "ViewModel-like" nội bộ. Mặc dù không được hỗ trợ ngay từ đầu cho coroutine và trạng thái đã lưu, nhưng retain có thể đóng vai trò là thành phần cơ bản cho vòng đời của các đối tượng tương tự ViewModel với các tính năng được xây dựng dựa trên đó. Thông tin cụ thể về cách thiết kế một thành phần như vậy nằm ngoài phạm vi của hướng dẫn này.
|
|
|
|---|---|---|
Phân phạm vi |
Không có giá trị được chia sẻ; mỗi giá trị được giữ lại tại và liên kết với một điểm cụ thể trong hệ phân cấp thành phần. Việc giữ lại cùng một loại ở một vị trí khác luôn hoạt động trên một thực thể mới. |
|
Huỷ |
Khi rời khỏi hệ phân cấp thành phần vĩnh viễn |
Khi |
Chức năng bổ sung |
Có thể nhận lệnh gọi lại khi đối tượng ở trong hoặc không ở trong hệ phân cấp thành phần |
|
Chủ sở hữu: |
|
|
Trường hợp sử dụng |
|
|
Kết hợp retain và rememberSaveable hoặc rememberSerializable
Đôi khi, một đối tượng cần có vòng đời kết hợp của cả retained và rememberSaveable hoặc rememberSerializable. Đây có thể là dấu hiệu cho thấy đối tượng của bạn phải là ViewModel, có thể hỗ trợ trạng thái đã lưu như mô tả trong hướng dẫn Mô-đun trạng thái đã lưu cho ViewModel.
Bạn có thể sử dụng retain và rememberSaveable hoặc rememberSerializable cùng lúc. Việc kết hợp chính xác cả hai vòng đời sẽ làm tăng đáng kể độ phức tạp.
Bạn nên sử dụng mẫu này như một phần của các mẫu kiến trúc nâng cao và tuỳ chỉnh hơn, đồng thời chỉ khi tất cả các điều kiện sau đây đều đúng:
- Bạn đang xác định một đối tượng bao gồm hỗn hợp các giá trị phải được giữ lại hoặc lưu (ví dụ: một đối tượng theo dõi hoạt động đầu vào của người dùng và bộ nhớ đệm trong bộ nhớ không thể ghi vào đĩa)
- Trạng thái của bạn được phân phạm vi cho một thành phần kết hợp và không phù hợp với phạm vi singleton hoặc vòng đời của
ViewModel
Khi tất cả các điều kiện này đều đúng, bạn nên chia lớp của mình thành 3 phần: Dữ liệu đã lưu, dữ liệu được giữ lại và đối tượng "trung gian" không có trạng thái riêng và uỷ quyền cho các đối tượng được giữ lại và đã lưu để cập nhật trạng thái cho phù hợp. Mẫu này có hình dạng sau:
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
Bằng cách tách trạng thái theo vòng đời, việc tách biệt trách nhiệm và bộ nhớ sẽ trở nên rất rõ ràng. Việc dữ liệu đã lưu không thể bị thao tác bởi dữ liệu được giữ lại là có chủ ý, vì điều này ngăn chặn trường hợp cập nhật dữ liệu đã lưu khi gói savedInstanceState đã được thu thập và không thể cập nhật. Điều này cũng cho phép kiểm thử các trường hợp tạo lại bằng cách kiểm thử các hàm khởi tạo mà không cần gọi vào Compose hoặc mô phỏng việc tạo lại Hoạt động.
Hãy xem mẫu đầy đủ (RetainAndSaveSample.kt) để biết ví dụ hoàn chỉnh về
cách triển khai mẫu này.
Ghi nhớ theo vị trí và bố cục thích ứng
Các ứng dụng Android có thể hỗ trợ nhiều kiểu dáng, bao gồm điện thoại, điện thoại gập, máy tính bảng và máy tính để bàn. Các ứng dụng thường cần chuyển đổi giữa các kiểu dáng này bằng cách sử dụng bố cục thích ứng. Ví dụ: một ứng dụng chạy trên máy tính bảng có thể hiển thị chế độ xem chi tiết danh sách 2 cột, nhưng có thể chuyển đổi giữa danh sách và trang chi tiết khi hiển thị trên màn hình điện thoại nhỏ hơn.
Vì các giá trị được ghi nhớ và giữ lại được ghi nhớ theo vị trí, nên chúng chỉ được sử dụng lại nếu xuất hiện ở cùng một điểm trong hệ phân cấp thành phần. Khi bố cục của bạn thích ứng với các kiểu dáng khác nhau, chúng có thể thay đổi cấu trúc của hệ phân cấp thành phần và dẫn đến các giá trị bị quên.
Đối với các thành phần ngay từ đầu như ListDetailPaneScaffold và NavDisplay (từ Jetpack Navigation 3), đây không phải là vấn đề và trạng thái của bạn sẽ được duy trì trong suốt các thay đổi về bố cục. Đối với các thành phần tuỳ chỉnh thích ứng với kiểu dáng, hãy đảm bảo rằng trạng thái không bị ảnh hưởng bởi các thay đổi về bố cục bằng cách thực hiện một trong những thao tác sau:
- Đảm bảo rằng các thành phần kết hợp có trạng thái luôn được gọi ở cùng một vị trí trong hệ phân cấp thành phần. Triển khai bố cục thích ứng bằng cách thay đổi logic bố cục thay vì di chuyển các đối tượng trong hệ phân cấp thành phần.
- Sử dụng
MovableContentđể di chuyển các thành phần kết hợp có trạng thái một cách linh hoạt. Các thực thể củaMovableContentcó thể di chuyển các giá trị được ghi nhớ và giữ lại từ vị trí cũ sang vị trí mới.
Ghi nhớ các hàm nhà máy
Mặc dù giao diện người dùng Compose được tạo thành từ các hàm có khả năng kết hợp, nhưng nhiều đối tượng sẽ tham gia vào việc tạo và sắp xếp một thành phần. Ví dụ phổ biến nhất về điều này
là các đối tượng có khả năng kết hợp phức tạp xác định trạng thái riêng của chúng, chẳng hạn như LazyList,
chấp nhận LazyListState.
Khi xác định các đối tượng tập trung vào Compose, bạn nên tạo hàm remember để xác định hành vi ghi nhớ dự kiến, bao gồm cả vòng đời và các dữ liệu đầu vào chính. Điều này cho phép người dùng trạng thái của bạn tự tin tạo các thực thể trong hệ phân cấp thành phần sẽ tồn tại và bị vô hiệu hoá như dự kiến. Khi xác định hàm nhà máy có khả năng kết hợp, hãy làm theo các nguyên tắc sau:
- Thêm tiền tố
remembervào tên hàm. Bạn có thể tuỳ ý sử dụng tiền tốretainnếu việc triển khai hàm phụ thuộc vào đối tượng đang đượcretainedvà API sẽ không bao giờ phát triển để dựa vào một biến thể khác củaremember, sử dụng tiền tốretainthay thế. - Sử dụng
rememberSaveablehoặcrememberSerializablenếu bạn chọn duy trì trạng thái và có thể viết cách triển khaiSaverchính xác. - Tránh các hiệu ứng phụ hoặc khởi chạy các giá trị dựa trên
CompositionLocalcó thể không liên quan đến việc sử dụng. Hãy nhớ rằng vị trí tạo trạng thái của bạn có thể không phải là nơi sử dụng trạng thái đó.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }