Trạng thái trong ứng dụng là giá trị bất kỳ có thể thay đổi theo thời gian. Đây là định nghĩa rất rộng và bao gồm mọi thứ từ cơ sở dữ liệu Room cho đến một biến trong một lớp (class).
Tất cả ứng dụng Android đều cho người dùng thấy trạng thái. Sau đây là một số ví dụ về trạng thái trong ứng dụng Android:
- Một thanh thông báo nhanh cho biết thời điểm không thể thiết lập kết nối mạng.
- Một bài đăng trên blog và các bình luận liên quan.
- Ảnh động gợn sóng trên các nút phát khi người dùng nhấp vào.
- Hình dán mà người dùng có thể vẽ lên hình ảnh.
Jetpack Compose giúp bạn hiểu rõ vị trí và cách thức lưu trữ cũng như dùng trạng thái trong một ứng dụng Android. Tài liệu hướng dẫn này tập trung vào hoạt động kết nối giữa các trạng thái và thành phần kết hợp (composable), đồng thời tập trung vào những API mà Jetpack Compose cung cấp để xử lý trạng thái dễ dàng hơn.
Trạng thái và thành phần
Compose mang tính khai báo và vì vậy, cách duy nhất để cập nhật Compose là gọi cùng một thành phần kết hợp (composable) với đối số mới. Các đối số này là đại diện cho trạng thái giao diện người dùng. Mỗi khi một trạng thái được cập nhật, một lượt tái cấu trúc (recomposition) diễn ra. Do đó, những thành phần như TextField
sẽ không tự động cập nhật như đối với khung hiển thị dựa trên XML bắt buộc. Một thành phần kết hợp phải được thông báo rõ ràng về trạng thái mới để cập nhật tương ứng.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
Nếu chạy mã này và cố gắng nhập văn bản, bạn sẽ thấy không có điều gì xảy ra. Nguyên nhân là do TextField
không tự cập nhật, mà sẽ cập nhật khi tham số value
của nó thay đổi. Điều này là do cách hoạt động của tính năng kết hợp và tái cấu trúc trong Compose.
Để tìm hiểu thêm về quá trình cấu trúc ban đầu và tái cấu trúc, hãy xem bài viết Cách suy nghĩ trong Compose.
Trạng thái trong thành phần kết hợp
Các hàm có khả năng kết hợp có thể sử dụng API remember
để lưu trữ đối tượng trong bộ nhớ. Một giá trị do remember
tính toán được lưu trữ trong Cấu trúc (Composition) trong quá trình cấu trúc ban đầu. Giá trị đã lưu trữ được trả về trong quá trình tái cấu trúc.
Bạn có thể dùng remember
để lưu trữ cả đối tượng có thể thay đổi và không thể thay đổi.
mutableStateOf
tạo ra MutableState<T>
có thể quan sát. Đây là một loại đối tượng có thể quan sát được tích hợp với thời gian chạy Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Mọi thay đổi đối với value
sẽ lên lịch tái cấu trúc mọi hàm có khả năng kết hợp có thể đọc value
.
Có 3 cách để khai báo đối tượng MutableState
trong một thành phần kết hợp:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
Các thông tin khai báo này là tương đương và được cung cấp dưới dạng cú pháp dễ hiểu theo mục đích sử dụng của trạng thái. Bạn nên chọn định dạng tạo ra mã dễ đọc nhất trong thành phần kết hợp mà bạn đang viết.
Cú pháp uỷ quyền (delegate syntax) by
yêu cầu các lần nhập sau:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Bạn có thể sử dụng giá trị đã ghi nhớ làm tham số cho các thành phần kết hợp khác hoặc thậm chí là logic trong các câu lệnh để thay đổi thành phần kết hợp được hiển thị. Ví dụ: nếu bạn không muốn hiện lời chào nếu phần tên trống, hãy sử dụng trạng thái trong câu lệnh if
:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
Mặc dù remember
giúp bạn giữ lại trạng thái trên các lần tái cấu trúc, trạng thái này sẽ không được giữ lại khi bạn thay đổi cấu hình. Để làm được điều này, bạn phải sử dụng rememberSaveable
. rememberSaveable
tự động lưu mọi giá trị có thể lưu trong Bundle
. Đối với các giá trị khác, bạn có thể chuyển vào một đối tượng lưu tuỳ chỉnh.
Các loại trạng thái được hỗ trợ khác
Compose không yêu cầu bạn phải dùng MutableState<T>
để giữ trạng thái; công cụ này hỗ trợ các loại đối tượng phát ra dữ liệu khác. Trước khi đọc một loại đối tượng khác có thể quan sát trong Compose, bạn phải chuyển đổi đối tượng đó thành State<T>
để các thành phần kết hợp có thể tự động kết hợp lại khi trạng thái thay đổi.
Compose cung cấp các hàm để tạo State<T>
từ những loại đối tượng có thể quan sát phổ biến được dùng trong các ứng dụng Android. Trước khi sử dụng những tiện ích tích hợp này, hãy thêm (các) cấu phần phần mềm phù hợp như được nêu dưới đây:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
thu thập các giá trị từFlow
theo cách nhận biết vòng đời, cho phép ứng dụng tiết kiệm tài nguyên ứng dụng. Hàm này biểu thị giá trị được tạo ra gần đây nhất từState
trong Compose. Bạn nên dùng API này để thu thập các luồng trên ứng dụng Android.Tệp
build.gradle
cần phải có phần phụ thuộc sau (phải sử dụng phiên bản 2.6.0-beta01 trở lên):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.5"
}
-
collectAsState
tương tự nhưcollectAsStateWithLifecycle
, vì hàm này cũng thu thập các giá trị từFlow
và chuyển đổi giá trị đó thànhState
trong Compose.Hãy dùng
collectAsState
cho mã không phụ thuộc vào nền tảng thay vìcollectAsStateWithLifecycle
, vốn chỉ dành cho Android.collectAsState
không cần có thêm phần phụ thuộc vì hàm này có sẵn trongcompose-runtime
. -
observeAsState()
bắt đầu quan sátLiveData
này và biểu thị các giá trị của lớp này thông quaState
.Tệp
build.gradle
phải có phần phụ thuộc sau đây:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.3"
}
-
subscribeAsState()
là các hàm mở rộng giúp biến đổi luồng phản ứng của RxJava2 (ví dụ:Single
,Observable
,Completable
) thànhState
trong Compose.Tệp
build.gradle
phải có phần phụ thuộc sau đây:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.3"
}
-
subscribeAsState()
là các hàm mở rộng giúp biến đổi luồng phản ứng của RxJava3 (ví dụ:Single
,Observable
,Completable
) thànhState
trong Compose.Tệp
build.gradle
phải có phần phụ thuộc sau đây:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.3"
}
Có trạng thái so với không có trạng thái
Một thành phần kết hợp sử dụng remember
để lưu trữ một đối tượng sẽ tạo trạng thái nội bộ, giúp thành phần kết hợp có trạng thái. HelloContent
là một ví dụ về thành phần kết hợp có trạng thái vì hàm này giữ và sửa đổi trạng thái name
nội tại. Điều này có thể hữu ích trong trường hợp phương thức gọi không cần kiểm soát trạng thái và có thể sử dụng mà không phải tự quản lý trạng thái. Tuy nhiên, các thành phần kết hợp với trạng thái nội bộ có xu hướng ít có khả năng tái sử dụng và khó kiểm thử hơn.
Thành phần kết hợp không có trạng thái (stateless) là thành phần kết hợp không thuộc một trạng thái nào. Một cách dễ dàng để đạt được trạng thái không có trạng thái là sử dụng tính năng chuyển trạng thái lên trên (state hoisting).
Khi phát triển các thành phần kết hợp có thể sử dụng lại, bạn thường muốn hiện cả phiên bản trạng thái và phiên bản không có trạng thái của cùng một thành phần kết hợp. Phiên bản có trạng thái sẽ hữu ích cho trình gọi không quan tâm đến trạng thái, và phiên bản không trạng thái cần thiết cho trình gọi cần kiểm soát hoặc di chuyển trạng thái lên trên.
Chuyển trạng thái lên trên
Tính năng chuyển trạng thái lên trên (state hoisting) trong Compose là một dạng chuyển đổi trạng thái cho phương thức gọi của một thành phần kết hợp khiến nó trở thành không trạng thái. Mô hình chung để di chuyển trạng thái lên trên trong Jetpack Compose là thay thế biến trạng thái bằng 2 tham số:
value: T
: giá trị hiện tại để hiển thịonValueChange: (T) -> Unit
: một sự kiện yêu cầu thay đổi giá trị này, trong đóT
là giá trị mới được đề xuất
Tuy nhiên, bạn không bị giới hạn ở onValueChange
. Nếu các sự kiện cụ thể hơn phù hợp với thành phần kết hợp, bạn nên xác định sự kiện bằng cách sử dụng lambda.
Trạng thái được di chuyển lên trên theo cách này có một số thuộc tính quan trọng:
- Một nguồn đáng tin cậy (single source of truth): Bằng cách di chuyển trạng thái thay vì sao chép, chúng tôi đảm bảo rằng chỉ có một nguồn thông tin duy nhất. Điều này giúp tránh các lỗi.
- Được đóng gói (encapsulated): Chỉ các thành phần kết hợp có trạng thái mới có thể sửa đổi trạng thái của chúng. Nó có tính nội bộ hoàn toàn.
- Có thể chia sẻ (shareable): Bạn có thể chia sẻ trạng thái được di chuyển lên trên với nhiều thành phần kết hợp. Nếu bạn muốn đọc
name
trong một thành phần kết hợp khác, việc di chuyển trạng thái lên trên sẽ cho phép bạn làm việc đó. - Có thể chắn (interceptable): phương thức gọi đến các thành phần kết hợp không trạng thái có thể quyết định bỏ qua hoặc sửa đổi các sự kiện trước khi thay đổi trạng thái.
- Được tách riêng (decoupled): trạng thái của các thành phần kết hợp không có trạng thái có thể được lưu trữ ở bất cứ đâu. Ví dụ: bạn hiện có thể di chuyển
name
sangViewModel
.
Trong trường hợp ví dụ, bạn trích xuất name
và onValueChange
ra HelloContent
rồi di chuyển chúng lên trên đến một thành phần kết hợp HelloScreen
bằng lệnh gọi HelloContent
.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
Bằng cách nâng trạng thái ra khỏi HelloContent
, sẽ dễ dàng cho việc lý giải thành phần kết hợp, tái sử dụng trong nhiều tình huống và kiểm thử. HelloContent
được tách riêng khỏi cách lưu trữ trạng thái của tệp. Việc tách riêng có nghĩa là nếu sửa đổi hoặc thay thế HelloScreen
, bạn không phải thay đổi cách thức triển khai HelloContent
.
Mô hình mà trạng thái giảm và các sự kiện tăng lên được gọi là luồng dữ liệu một chiều (unidirectional). Trong trường hợp này, trạng thái giảm từ HelloScreen
xuống HelloContent
và các sự kiện tăng từ HelloContent
lên HelloScreen
. Bằng cách làm theo luồng dữ liệu một chiều, bạn có thể phân tách các thành phần có thể kết hợp hiển thị trạng thái trong giao diện người dùng khỏi các phần của ứng dụng lưu trữ và thay đổi trạng thái.
Xem trang Vị trí chuyển trạng thái lên trên để tìm hiểu thêm.
Khôi phục trạng thái trong Compose
API rememberSaveable
hoạt động tương tự như remember
vì API này giữ lại trạng thái trên các lần kết hợp lại cũng như trong quá trình tạo lại hoạt động hoặc quy trình bằng cơ chế trạng thái của thực thể đã lưu. Ví dụ: điều này xảy ra khi màn hình được xoay.
Các cách lưu trữ trạng thái
Tất cả loại dữ liệu được thêm vào Bundle
sẽ được lưu tự động. Nếu muốn lưu nội dung nào đó mà không thể thêm vào Bundle
, bạn có thể làm theo các cách sau:
Parcelize
Cách đơn giản nhất là thêm chú thích @Parcelize
vào đối tượng. Đối tượng sẽ đóng gói được và nhóm được. Ví dụ: mã này tạo một loại dữ liệu City
có thể đóng gói và lưu dữ liệu vào trạng thái.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
Nếu vì lý do nào đó @Parcelize
không phù hợp, bạn có thể sử dụng mapSaver
để xác định quy tắc của riêng mình nhằm chuyển đổi một đối tượng thành một tập hợp giá trị mà hệ thống có thể lưu vào Bundle
.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
Để tránh phải xác định các khoá cho bản đồ, bạn cũng có thể dùng listSaver
và sử dụng các chỉ mục làm khoá:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Phần tử giữ trạng thái trong Compose
Có thể quản lý quá trình chuyển trạng thái lên trên đơn giản trong chính các hàm có khả năng kết hợp. Tuy nhiên, nếu số lượng trạng thái cần theo dõi tăng lên hoặc logic để thực hiện trong các hàm có khả năng kết hợp phát sinh, bạn nên uỷ quyền trách nhiệm về logic và trạng thái cho các lớp khác: phần tử giữ trạng thái.
Hãy xem tài liệu về quá trình chuyển trạng thái lên trên (state hoisting) trong Compose hoặc tài liệu chung hơn là trang Phần tử giữ trạng thái và Trạng thái giao diện người dùng trong hướng dẫn về cấu trúc để tìm hiểu thêm.
Kích hoạt lại tính năng ghi nhớ các tính toán khi khoá thay đổi
API remember
thường được dùng cùng với MutableState
:
var name by remember { mutableStateOf("") }
Ở đây, việc sử dụng hàm remember
giúp giá trị MutableState
vẫn tiếp tục có hiệu lực khi kết hợp lại.
Nói chung, remember
nhận tham số lambda calculation
. Trong lần chạy đầu tiên, remember
sẽ gọi hàm lambda calculation
và lưu trữ kết quả. Trong quá trình kết hợp lại, remember
sẽ trả về giá trị được lưu trữ gần đây nhất.
Ngoài trạng thái lưu vào bộ nhớ đệm, bạn cũng có thể sử dụng remember
để lưu trữ đối tượng bất kỳ hoặc kết quả của một thao tác trong Cấu trúc (Composition) nếu tốn kém khi khởi chạy hoặc tính toán. Bạn có thể không muốn lặp lại phép tính này trong mỗi quá trình kết hợp lại.
Ví dụ như việc tạo đối tượng ShaderBrush
này có thể là một thao tác tốn kém:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
lưu trữ giá trị cho đến khi thoát khỏi Cấu trúc. Tuy nhiên, có một cách để vô hiệu hoá giá trị đã lưu vào bộ nhớ đệm. API remember
cũng lấy tham số key
hoặc keys
. Nếu bất kỳ khoá nào trong số này thay đổi thì lần tiếp theo khi hàm kết hợp lại, remember
sẽ vô hiệu hoá bộ nhớ đệm và thực thi lại khối lambda tính toán. Cơ chế này cho phép bạn kiểm soát toàn thời gian của một đối tượng trong Cấu trúc. Phương pháp tính toán này vẫn có hiệu lực cho đến khi giá trị nhập thay đổi, thay vì cho đến khi giá trị được ghi nhớ bị xoá khỏi Cấu trúc.
Các ví dụ sau đây minh hoạ cách hoạt động của cơ chế này.
Trong đoạn mã này, ShaderBrush
được tạo và sử dụng làm màu nền của thành phần kết hợp Box
. remember
lưu trữ thực thể ShaderBrush
vì việc tạo lại rất tốn kém, như chúng tôi đã giải thích trước đó. remember
lấy avatarRes
làm tham số key1
, là hình nền đã chọn. Nếu avatarRes
thay đổi, bút vẽ sẽ kết hợp lại với hình ảnh mới và áp dụng lại cho Box
. Điều này có thể xảy ra khi người dùng chọn một hình ảnh khác trong bộ chọn làm hình nền.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
Trong đoạn mã tiếp theo, trạng thái được chuyển lên trên lớp phần tử giữ trạng thái thuần tuý MyAppState
. Phương thức này hiển thị một hàm rememberMyAppState
để khởi chạy một phiên bản của lớp bằng cách dùng remember
. Việc hiển thị các hàm như vậy để tạo phiên bản tiếp tục có hiệu lực trong quá trình kết hợp lại là một mẫu phổ biến trong Compose. Hàm rememberMyAppState
nhận windowSizeClass
, đóng vai trò là tham số key
cho remember
. Nếu tham số này thay đổi, ứng dụng cần tạo lại phần tử giữ trạng thái thuần tuý có giá trị mới nhất. Điều này có thể xảy ra nếu người dùng xoay thiết bị chẳng hạn.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose sử dụng cách triển khai tương đương của lớp để quyết định xem một khoá đã thay đổi hay chưa và vô hiệu hoá giá trị được lưu trữ.
Lưu giữ trạng thái bằng các khoá khi kết hợp lại
API rememberSaveable
là một trình bao bọc xung quanh remember
có thể lưu trữ dữ liệu trong một Bundle
. API này cho phép trạng thái tồn tại không chỉ trong quá trình kết hợp lại, mà còn trong quá trình tạo lại hoạt động và sự kiện bị buộc tắt do hệ thống gây ra.
rememberSaveable
nhận các tham số input
cho cùng một mục đích khi remember
nhận keys
. Bộ nhớ đệm bị vô hiệu hoá khi bất kỳ dữ liệu đầu vào nào thay đổi. Vào lần tới hàm này kết hợp lại, rememberSaveable
sẽ thực thi lại khối lambda tính toán.
Trong ví dụ sau, rememberSaveable
lưu trữ userTypedQuery
cho đến khi typedQuery
thay đổi:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
Tìm hiểu thêm
Để tìm hiểu thêm về trạng thái và Jetpack Compose, hãy tham khảo thêm các tài nguyên sau đây:
Mẫu
Lớp học lập trình
Video
Blog
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Thiết kế giao diện Compose
- Lưu trạng thái giao diện người dùng trong Compose
- Những hiệu ứng phụ trong ứng dụng Compose