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. Khi đọc hết tài liệu, bạn sẽ:
- Biết được loại API nào nên sử 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.
- Biết 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.
- Biết 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ừ:
- 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:
- Đầu vào: Nguồn thay đổi trạng thái. Các nguồn đó có thể:
- 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
open
trênDrawerState
trong 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. Ví dụ: tin tức đã tải xong từ
NewsRepository
hoặc các sự kiện khác. - Kết hợp cả 2 nguồn 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 kinh doanh và/hoặc logic giao diện người dùng vào nguồn thay đổi trạng thái để xử lý các sự kiện của người dùng nhằm 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 | Bạn nên sử dụng các API không đồng bộ để 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. Ví dụ: Coroutine hoặc Flow trong Kotlin, RxJava hoặc các lệnh gọi lại bằng Ngôn ngữ lập trình Java. |
Đầu ra | Bạn nên sử dụng các API phần tử giữ dữ liệu có thể ghi nhận được để vô hiệu hoá và hiển thị lại giao diện người dùng khi trạng thái thay đổi. Ví dụ: StateFlow, Compose State hoặc LiveData. 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 |
Trong 2 giai đoạn trên, 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. Quy trình này nên:
- 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. Các cân nhắc về đầu ra của quy trình tạo trạng thái sẽ thay đổi tuỳ theo các View API (API khung hiển thị), chẳng hạn như Hệ thống khung hiển thị hoặc Jetpack Compose.
Đầ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 có thể cung cấp nguồn thay đổi trạng thái thông qua:
- 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
Sử dụng API MutableStateFlow
làm vùng chứa trạng thái có thể thay đổi và ghi nhận được. Trong các ứng dụng Jetpack Compose, bạn cũng có thể cân nhắc sử dụng mutableStateOf
, đặc biệt là khi làm việc với API văn bản của Compose. 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.
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,
)
}
}
}
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
}
}
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.
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))
}
}
}
}
}
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))
}
}
}
}
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à chạy 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 ngữ cảnh nền khác, bạn có thể thực hiện bằng cách sử dụng các API 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ứcupdate
như bình thường. - Khi sử dụng Trạng thái Compose, hãy dùng
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ụ: Trong đoạn mã DiceRollViewModel
dưới đây, giả sử rằng SlowRandom.nextInt()
là một hàm suspend
tính toán chuyên sâu cần được gọi từ Coroutine ràng buộc của CPU.
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,
)
}
}
}
}
}
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
}
}
}
}
}
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 kết hợp . 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.WhileSubscribed()
nếu quy trình chỉ 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.Lazily
nếu quy trình sẽ 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à các kiểu biến đổi khác để 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 bên dưới:
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, task ->
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 }
}
}
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, task ->
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
}
}
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. Trong ứng dụng Android, bạn có thể chọn sử dụng View (Khung hiển thị) hoặc Jetpack Compose. 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.
- Trạng thái có nên hiển thị 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 cho bất kỳ đầu vào và người tiêu dùng nào:
Đầu vào | Người tiêu dùng | Đầu ra |
---|---|---|
API một lần | Khung hiển thị | StateFlow hoặc LiveData |
API một lần | Compose | StateFlow hoặc Compose State |
API luồng | Khung hiển thị | StateFlow hoặc LiveData |
API luồng | Compose | StateFlow |
API một lần và API luồng | Khung hiển thị | StateFlow hoặc LiveData |
API một lần và API luồng | Compose | 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ộ.
Bạn nên khởi động từng phần của quy trình tạo trạng thái khi có thể để 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 điều này bằng cách sử dụng
đối số started
trong stateIn
. Trong trường hợp không thể áp dụng điều này,
định nghĩa một giá trị không thay đổi
Hàm initialize()
để 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ế:
Đề 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}