Ngày nay, giao diện người dùng hiếm khi ở dạng tĩnh. Trạng thái của giao diện người dùng thay đổi khi người dùng tương tác với giao diện người dùng hoặc khi ứng dụng cần hiển thị dữ liệu mới.
Tài liệu này đặt ra các nguyên tắc về việc tạo và quản lý trạng thái giao diện người dùng. Mục đích của hướng dẫn này là giúp bạn hiểu rõ những điều sau:
- Những API cần dùng để tạo trạng thái giao diện người dùng. Điều này phụ thuộc vào bản chất của các nguồn tạo ra sự thay đổi trạng thái có trong phần tử giữ trạng thái, theo các nguyên tắc luồng dữ liệu một chiều.
- Cách xác định phạm vi tạo trạng thái giao diện người dùng để nắm rõ tài nguyên hệ thống.
- Cách hiển thị trạng thái giao diện người dùng để giao diện người dùng sử dụng.
Về cơ bản, tạo trạng thái là việc tăng cường áp dụng dần những thay đổi này đối với trạng thái giao diện người dùng. Trạng thái luôn tồn tại và thay đổi do các sự kiện. Bảng dưới đây tóm tắt các điểm khác biệt giữa sự kiện và trạng thái:
| Sự kiện | Trạng thái |
|---|---|
| Tạm thời, không thể đoán trước và tồn tại trong một khoảng thời gian có hạn. | Luôn tồn tại. |
| Dữ kiện đầu vào của quá trình tạo trạng thái. | Là kết quả của quá trình tạo trạng thái. |
| Là sản phẩm của giao diện người dùng hoặc các nguồn khác. | Được giao diện người dùng sử dụng. |
Một mẹo tuyệt vời để ghi nhớ những điều trên là trạng thái – thời gian; sự kiện – thời điểm. Sơ đồ dưới đây giúp bạn hình dung về sự thay đổi theo thời gian của trạng thái khi sự kiện xảy ra. Các phần tử giữ trạng thái thích hợp sẽ xử lý sự kiện liên quan, từ đó dẫn đến sự thay đổi trạng thái:
Sự kiện có thể đến từ những nguồn sau:
- Người dùng: Khi họ tương tác với giao diện người dùng của ứng dụng.
- Các nguồn khác làm thay đổi trạng thái: Các API trình bày dữ liệu ứng dụng từ giao diện người dùng, miền hoặc các lớp dữ liệu như sự kiện hết thời gian chờ của thanh thông báo nhanh, các trường hợp sử dụng hoặc kho lưu trữ tương ứng.
Quy trình tạo trạng thái giao diện người dùng
Quy trình tạo trạng thái trong ứng dụng Android có thể được coi là một quy trình xử lý bao gồm những thành phần sau:
- Đầu vào: Nguồn thay đổi trạng thái. Các nguồn đó có thể là:
- Nằm trong lớp giao diện người dùng: Chẳng hạn như sự kiện do người dùng tạo (ví dụ: nhập tiêu đề cho "việc cần làm" trong ứng dụng quản lý công việc) hoặc từ các API cung cấp quyền truy cập vào logic giao diện người dùng dẫn đến sự thay đổi về trạng thái giao diện người dùng – ví dụ: gọi phương thức
opentrênDrawerStatetrong Jetpack Compose. - Bên ngoài lớp giao diện người dùng: Đây là các nguồn từ các lớp miền hoặc lớp dữ liệu gây ra những thay đổi đối với trạng thái giao diện người dùng, chẳng hạn như tin tức đã tải xong từ một
NewsRepositoryhoặc các sự kiện khác. - Kết hợp các yếu tố trên.
- Nằm trong lớp giao diện người dùng: Chẳng hạn như sự kiện do người dùng tạo (ví dụ: nhập tiêu đề cho "việc cần làm" trong ứng dụng quản lý công việc) hoặc từ các API cung cấp quyền truy cập vào logic giao diện người dùng dẫn đến sự thay đổi về trạng thái giao diện người dùng – ví dụ: gọi phương thức
- Phần tử giữ trạng thái: Là các kiểu áp dụng logic nghiệp vụ và logic giao diện người dùng vào nguồn thay đổi trạng thái, đồng thời xử lý các sự kiện của người dùng để tạo trạng thái giao diện người dùng.
- Đầu ra: Là trạng thái giao diện người dùng mà ứng dụng có thể hiển thị để cung cấp cho người dùng thông tin họ cần.
API tạo trạng thái
Có 2 API chính được dùng trong quá trình tạo trạng thái tuỳ thuộc vào giai đoạn của quy trình mà bạn đang thực hiện:
| Giai đoạn của quy trình | API |
|---|---|
| Đầu vào | Sử dụng các API không đồng bộ như Coroutine và Luồng để thực hiện công việc ngoài luồng giao diện người dùng, nhờ đó, giao diện người dùng sẽ không bị giật. |
| Đầu ra | Sử dụng các API phần tử giữ dữ liệu có thể ghi nhận được như Trạng thái Compose hoặc StateFlow để vô hiệu hoá và hiển thị lại giao diện người dùng khi trạng thái thay đổi. Phần tử giữ dữ liệu có thể ghi nhận được đảm bảo giao diện người dùng luôn có trạng thái giao diện người dùng để hiển thị trên màn hình. |
Việc lựa chọn API không đồng bộ cho đầu vào có ảnh hưởng lớn hơn đến bản chất của quy trình tạo trạng thái so với lựa chọn API có thể ghi nhận được cho đầu ra. Lý do là dữ liệu đầu vào mô tả cách xử lý có thể áp dụng cho quy trình.
Tập hợp quy trình tạo trạng thái
Các phần tiếp theo trình bày kỹ thuật tạo trạng thái phù hợp nhất với nhiều dữ liệu đầu vào và API đầu ra trùng khớp. Mỗi quy trình tạo trạng thái là một tổ hợp đầu vào và đầu ra, đồng thời phải là:
- Nhận biết được vòng đời: Trong trường hợp giao diện người dùng không hiển thị hoặc đang hoạt động, quy trình tạo trạng thái sẽ không dùng bất kỳ tài nguyên nào trừ phi được yêu cầu rõ ràng.
- Dễ dùng: Giao diện người dùng phải có thể dễ dàng hiển thị trạng thái giao diện người dùng đã tạo. Trong Jetpack Compose, việc sử dụng trạng thái là yếu tố cốt lõi của giao diện người dùng, vì các thành phần kết hợp có thể cập nhật dựa trên những thay đổi về trạng thái.
Đầu vào trong quy trình tạo trạng thái
Đầu vào trong quy trình tạo trạng thái cung cấp nguồn thay đổi trạng thái thông qua những cách sau:
- Thao tác một lần có thể đồng bộ hoặc không đồng bộ – ví dụ: lệnh gọi đến các hàm
suspend. - API luồng — ví dụ:
Flows. - Tất cả các câu trên.
Các phần sau đây trình bày cách bạn có thể tập hợp quy trình tạo trạng thái cho mỗi đầu vào ở trên.
API một lần dưới dạng nguồn thay đổi trạng thái
Quản lý trạng thái bằng các trình giữ dữ liệu có thể ghi nhận được. Sử dụng API mutableStateOf, đặc biệt là khi làm việc với API văn bản của Compose. Đối với hoạt động quản lý trạng thái phức tạp hơn hoặc khi tích hợp với các thành phần kiến trúc khác, hãy dùng API MutableStateFlow. Cả hai API đều cung cấp phương thức cho phép cập nhật an toàn không thể phân chia đối với các giá trị mà chúng lưu trữ, cho dù các bản cập nhật đồng bộ hay không đồng bộ.
Ví dụ: hãy xem xét thông tin cập nhật trạng thái trong một ứng dụng đơn giản về đổ xúc xắc. Mỗi lần người dùng đổ xúc xắc sẽ gọi phương thức Random.nextInt đồng bộ và kết quả sẽ được ghi vào trạng thái giao diện người dùng.
Trạng thái Compose
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Thay đổi trạng thái giao diện người dùng từ các lệnh gọi không đồng bộ
Đối với các thay đổi trạng thái đòi hỏi kết quả không đồng bộ, hãy chạy Coroutine trong CoroutineScope thích hợp. Điều này cho phép ứng dụng loại bỏ công việc khi CoroutineScope bị huỷ. Sau đó, phần tử giữ trạng thái sẽ ghi kết quả của lệnh gọi phương thức tạm ngưng vào API có thể ghi nhận được dùng để hiển thị trạng thái giao diện người dùng.
Ví dụ: hãy xem xét AddEditTaskViewModel trong mẫu Kiến trúc. Khi phương thức saveTask tạm ngưng lưu một tác vụ theo cách không đồng bộ, phương thức update trên MutableStateFlow sẽ cập nhật sự thay đổi trạng thái cho trạng thái giao diện người dùng.
Trạng thái Compose
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Thay đổi trạng thái giao diện người dùng từ các luồng trong nền
Bạn nên chạy Coroutine trên trình điều phối chính để tạo trạng thái giao diện người dùng, tức là bên ngoài khối withContext trong các đoạn mã dưới đây.
Tuy nhiên, nếu cần cập nhật trạng thái giao diện người dùng trong một ngữ cảnh nền khác, bạn có thể làm như sau:
- Sử dụng phương thức
withContextđể chạy Coroutine trong một ngữ cảnh đồng thời khác. - Khi sử dụng
MutableStateFlow, hãy dùng phương thứcupdatenhư bình thường. - Khi sử dụng Trạng thái Compose, hãy dùng phương thức
Snapshot.withMutableSnapshotđể đảm bảo cập nhật không thể phân chia cho Trạng thái trong ngữ cảnh đồng thời.
Ví dụ: Giả sử trong đoạn mã DiceRollViewModel dưới đây, SlowRandom.nextInt là một hàm suspend tính toán chuyên sâu cần được gọi từ một Coroutine ràng buộc của CPU.
Trạng thái Compose
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
API luồng dưới dạng nguồn thay đổi trạng thái
Đối với các nguồn thay đổi trạng thái tạo ra nhiều giá trị theo thời gian trong luồng, việc tổng hợp đầu ra của tất cả các nguồn thành một tổng thể nhất quán là phương pháp đơn giản để tạo trạng thái.
Khi sử dụng Luồng Kotlin, bạn có thể đạt được điều này bằng hàm combine.
Bạn có thể xem ví dụ về vấn đề này trong mẫu"Now in Android" trong InterestsViewModel:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
Việc sử dụng toán tử stateIn để tạo StateFlows sẽ giúp giao diện người dùng kiểm soát chặt chẽ hơn hoạt động của quy trình tạo trạng thái vì giao diện này có thể chỉ cần hoạt động khi giao diện người dùng hiển thị.
- Sử dụng
SharingStarted.WhileSubscribednếu quy trình chỉ cần hoạt động khi giao diện người dùng hiển thị trong khi thu thập luồng theo cách nhận biết vòng đời. - Sử dụng
SharingStarted.Lazilynếu quy trình cần hoạt động miễn là người dùng có thể quay lại giao diện người dùng, tức là giao diện người dùng nằm trong ngăn xếp lui hoặc trong một thẻ khác ngoài màn hình.
Trong trường hợp không áp dụng tính năng tổng hợp các nguồn trạng thái dựa trên luồng, các API luồng như Kotlin Flows (Luồng Kotlin) sẽ cung cấp nhiều kiểu biến đổi như hợp nhất, làm phẳng, v.v. để giúp xử lý luồng vào trạng thái giao diện người dùng.
API một lần và API luồng dưới dạng các nguồn thay đổi trạng thái
Trong trường hợp quy trình tạo trạng thái phụ thuộc vào cả lệnh gọi một lần và luồng dưới dạng nguồn thay đổi trạng thái, thì luồng là điều kiện ràng buộc xác định. Do đó, hãy chuyển đổi các lệnh gọi một lần thành API luồng hoặc chuyển đầu ra của chúng vào các luồng và tiếp tục xử lý như mô tả trong mục luồng ở trên.
Với luồng, điều này thường có nghĩa là tạo một hoặc nhiều thực thể MutableStateFlow riêng tư để áp dụng các thay đổi trạng thái. Bạn cũng có thể tạo quy trình tổng quan nhanh từ trạng thái Compose.
Hãy xem xét TaskDetailViewModel trong kho lưu trữ architecture-samples. Trạng thái giao diện người dùng phụ thuộc vào một luồng cho tác vụ hiện tại (_task) và một nguồn một lần (_isTaskDeleted) sẽ cập nhật khi tác vụ bị xoá. Cờ này là cần thiết để phân biệt giữa trường hợp không tìm thấy một tác vụ trong cơ sở dữ liệu do mã nhận dạng không chính xác và trường hợp không tìm thấy tác vụ do người dùng vừa xoá tác vụ đó:
Trạng thái Compose
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, taskAsync ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, taskAsync ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Các loại đầu ra trong quy trình tạo trạng thái
Việc lựa chọn API đầu ra cho trạng thái giao diện người dùng và bản chất của bản trình bày phụ thuộc phần lớn vào API mà ứng dụng dùng để hiển thị giao diện người dùng, chẳng hạn như Compose. Jetpack Compose là bộ công cụ hiện đại, được khuyên dùng để xây dựng giao diện người dùng gốc. Sau đây là những yếu tố nên cân nhắc:
- Đọc trạng thái theo cách nhận biết được vòng đời.
- Có nên hiển thị trạng thái trong một hoặc nhiều trường từ phần tử giữ trạng thái hay không.
Bảng sau đây tóm tắt những API cần sử dụng trong quy trình tạo trạng thái của bạn khi dùng Jetpack Compose:
| Đầu vào | Đầu ra |
|---|---|
| API một lần | StateFlow hoặc Compose State |
| API luồng | StateFlow |
| API một lần và API luồng | StateFlow |
Khởi động quy trình tạo trạng thái
Việc khởi động quy trình tạo trạng thái bao gồm việc đặt các điều kiện ban đầu để chạy quy trình đó. Điều này có thể liên quan đến việc cung cấp các giá trị đầu vào ban đầu cần để bắt đầu quy trình, chẳng hạn như id để xem chi tiết về một tin bài hoặc bắt đầu một quá trình tải không đồng bộ.
Khi có thể, hãy khởi động từng phần của quy trình tạo trạng thái để tiết kiệm tài nguyên hệ thống. Trên thực tế, việc này thường có nghĩa là bạn sẽ phải đợi cho đến khi có một đối tượng sử dụng đầu ra. API Flow cho phép thực hiện việc này bằng đối số started trong phương thức stateIn. Trong trường hợp không áp dụng được, hãy xác định hàm initialize không thay đổi để bắt đầu quy trình tạo trạng thái một cách rõ ràng như minh hoạ trong đoạn mã sau:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
Mẫu
Các mẫu sau đây của Google minh hoạ quá trình tạo trạng thái trong lớp giao diện người dùng. Hãy khám phá những mẫu đó để xem hướng dẫn này trong thực tế:
Tài nguyên khác
Để biết thêm thông tin về trạng thái giao diện người dùng, hãy xem các tài nguyên bổ sung sau:
Tài liệu
Xem nội dung
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Lớp giao diện người dùng
- Xây dựng ứng dụng có chế độ ngoại tuyến
- Phần tử giữ trạng thái và trạng thái giao diện người dùng {:#mad-arch}