Kỹ sư quan hệ với nhà phát triển
Thời gian chạy ảnh chụp nhanh, tính năng theo dõi phần phụ thuộc tự động và cập nhật nguyên tử của Compose không nhất thiết phải dừng ở lớp giao diện người dùng. Jetpack AppState cho phép bạn di chuyển trạng thái Compose lên trên, giúp đơn giản hoá những gì xuất hiện trên màn hình bằng cách chuyển trạng thái lên trên và lấy trạng thái bằng hệ thống tổng quan nhanh trước khi trạng thái đó xuất hiện trên giao diện người dùng.
Trong buổi này, bạn sẽ tìm hiểu về hai thư viện cơ bản của AppState. Chúng ta sẽ tìm hiểu sâu về StateStore để nâng trạng thái và Transform để sử dụng thành phần nhằm lấy trạng thái bằng luồng điều khiển Kotlin thuần tuý, giúp loại bỏ các chuỗi kết hợp Flow phức tạp và đồng bộ hoá giao diện người dùng một cách dễ dàng.
combine, đồng thời hỗ trợ việc trả về sớm và các dẫn xuất trạng thái lồng nhau.Bản chép lời
Xin chào và chào mừng bạn đến với bài viết Đưa trạng thái Compose ra ngoài giao diện người dùng bằng Jetpack AppState.
Tôi là Ben, kỹ sư quan hệ nhà phát triển, làm việc với các thư viện cấu trúc Jetpack.
Tôi là Elif, một trong những kỹ sư phát triển thư viện kiến trúc ứng dụng.
Hôm nay, chúng tôi sẽ nói về tương lai của Compose, khi Compose không chỉ dừng lại ở giao diện người dùng mà còn mở rộng sang các lớp dữ liệu và miền của ứng dụng.
Như các bạn đã biết, Compose là bộ công cụ được đề xuất để phát triển giao diện người dùng trên Android.
Từ điện thoại và máy tính bảng cho đến tiện ích, đồng hồ, TV và thiết bị XR, Compose đều hỗ trợ tất cả.
Tuy nhiên, khi ứng dụng của bạn phát triển trên các nền tảng giao diện người dùng và thành phần nền này, việc quản lý trạng thái phía sau chúng sẽ trở nên khó khăn hơn, đặc biệt là xoay quanh hai điểm khó khăn phổ biến.
Đầu tiên là lấy trạng thái phức tạp.
Việc kết hợp nhiều luồng dữ liệu, dữ liệu đầu vào của giao diện người dùng cục bộ và trạng thái môi trường xung quanh (chẳng hạn như kết nối mạng) thường biến thành một mớ hỗn độn các toán tử như combine, flatMapLatest và distinctUntilChanged.
Và thứ hai là chia sẻ trạng thái giữa các thành phần: giữ cho hoạt động, dịch vụ, tiện ích ứng dụng và nhiều thành phần khác luôn đồng bộ, đồng thời đảm bảo rằng trạng thái đó có thể tồn tại trong quá trình bị buộc tắt khi cần.
Khi nhìn lại những thách thức này, chúng tôi đã xem xét cách xây dựng Compose.
Hầu hết mọi người đều nghĩ Compose là một bộ công cụ giao diện người dùng, nhưng thực ra nó có hai lớp riêng biệt: Giao diện người dùng Compose ở trên và Thời gian chạy Compose ở dưới.
Mặc dù Giao diện người dùng Compose xử lý bố cục, bản vẽ và dữ liệu đầu vào, nhưng Compose Runtime là một công cụ đa năng để quản lý trạng thái và kết hợp lại.
Lớp này không phụ thuộc vào giao diện người dùng.
Và nhận thức đó đã giúp chúng tôi xây dựng Jetpack AppState.
Đây là một nhóm thư viện tập trung vào việc khai thác tối đa trạng thái của Compose ngoài giao diện người dùng.
Nhưng trước khi tìm hiểu sâu về AppState, hãy đảm bảo chúng ta có cùng cách hiểu khi nói về trạng thái Compose.
Và câu trả lời là androidx.compose.runtime.State.
/**
* A value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value.
*/
@Stable
public interface State<out T> {
public val value: T
}Và giao diện mà bạn có thể quen thuộc hơn, MutableState.
/**
* A mutable value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value. When the
* [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
* will be scheduled. If [value] is written to with the same value, no recompositions will be
* scheduled.
*/
@Stable
public interface MutableState<T> : State<T> {
override var value: T
public operator fun component1(): T
public operator fun component2(): (T) -> Unit
}Về cơ bản, đây chỉ là những vùng chứa cơ bản.
Điều giúp các hàm này thực sự hoạt động là hệ thống ảnh chụp nhanh của Compose.
Vì vậy, khi nói đến trạng thái Compose, chúng ta thường muốn nói đến Trạng thái ảnh chụp nhanh.
Vì vậy, khi gọi mutableStateOf, bạn đang tạo một vùng chứa định tuyến các thao tác đọc và ghi thông qua hệ thống ảnh chụp nhanh.
Và bất cứ khi nào mã của bạn chạy bên trong một trình quan sát ảnh chụp nhanh, chẳng hạn như một thành phần, bạn sẽ nhận được hai thuộc tính mà một luồng như Flow không cung cấp cho bạn.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
}Đầu tiên là tính năng theo dõi lượt đọc tự động và linh động.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}Vì vậy, tự động có nghĩa là không cần đăng ký hoặc thu thập.
Chỉ đọc .value là gói thuê bao.
Vì vậy, nếu xem xét Counter ở đây, việc đọc count.value trực tiếp bên trong composable sẽ báo cáo cho composition đã đọc, đồng thời đăng ký Counter trong danh sách trình đọc.
Giờ hãy xem AddButton.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}
@Composable
fun AddButton(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Add")
}
}Hàm này chỉ ghi để tính lượt nhấp.
Vì vậy, đây không phải là máy đọc.
Khi người dùng nhấp vào AddButton và sửa đổi số lượng, hệ thống ảnh chụp nhanh sẽ theo dõi hoạt động ghi đó, cho phép thành phần kết hợp biết để vô hiệu hoá Bộ đếm, nhưng không vô hiệu hoá AddButton.
Còn động có nghĩa là quá trình theo dõi lượt đọc diễn ra trong thời gian chạy dựa trên đường dẫn mã thực sự chạy.
@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(true) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}Vì vậy, ở đây, chúng ta sẽ bảo vệ văn bản của bộ đếm bằng cờ isVisible.
Khi isVisible là true, cả isVisible và số lượng đều được đọc.
Vì vậy, Counter được đăng ký làm trình đọc của cả hai.
Và khi isVisible là false, nếu nhánh if không chạy, count sẽ không được đọc, vì vậy Counter chỉ được đăng ký làm trình đọc của isVisible, chứ không phải count.
@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(false) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}Vì vậy, khi AddButton ghi vào số lượng, không còn gì để vô hiệu hoá.
Và giờ đây, thuộc tính thứ hai là không phải lựa chọn giữa tính nhất quán và độ chi tiết.
Tính nhất quán có nghĩa là khi bạn cập nhật nhiều giá trị trong một bản tổng quan nhanh, những thay đổi đó sẽ được tách biệt trong khi mã của bạn chạy, ngay cả khi một luồng khác đang ghi vào các giá trị đó cùng một lúc.
Và những nội dung cập nhật đó sẽ xuất hiện cùng một lúc khi ảnh chụp nhanh hoàn tất.
Tính chi tiết có nghĩa là các thuộc tính riêng biệt có thể được khai báo và theo dõi riêng lẻ, đặc biệt là khi chúng không liên quan đến nhau.
Vì vậy, việc cập nhật một thuộc tính chỉ làm mất hiệu lực của các trình đọc thực sự phụ thuộc vào thuộc tính đó.
Hãy cùng xem xét điều này trong một số mã thực tế.
Ví dụ: chúng ta sẽ xem xét một trò chơi có 3 trạng thái: tên cấp độ, thời gian sống sót và điểm số.
val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0Để bắt đầu, hãy xem xét một cách triển khai Flow chọn tính nhất quán thay vì tính chi tiết bằng cách gói mọi thứ vào một lớp GameUiState.
data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)Điều đó có nghĩa là mỗi khi updateScore chạy, bạn sẽ sao chép toàn bộ GameUiState để cả timeSurvived và điểm số đều có thể cập nhật cùng nhau theo cách đồng bộ.
data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}Trong giao diện người dùng, GameScreen thu thập trạng thái giao diện người dùng và đọc từ đó để truyền các thuộc tính có liên quan xuống Header và ScoreBadge.
data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Column {
Header(levelName = state.levelName)
ScoreBadge(
timeSurvived = state.timeSurvived,
score = state.score
)
}
}Vì GameScreen đọc mọi trạng thái, nên mỗi lệnh gọi đến updateScore đều làm mất hiệu lực của GameScreen, mặc dù dữ liệu của tiêu đề không bao giờ thay đổi.
Bây giờ, bạn có thể chia mọi thuộc tính thành Luồng riêng để có được độ chi tiết, nhưng sau đó bạn sẽ mất tính nhất quán vì một luồng khác có thể cập nhật giá trị trong khi khối cập nhật khác đang chạy.
Bây giờ, hãy xem xét một cách triển khai khác dựa trên trạng thái.
@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}Trong ví dụ này, chúng ta lại sử dụng GameUiState.
Nhưng trong trường hợp này, có một chút khác biệt vì mỗi thuộc tính đều uỷ quyền cho mutableStateOf.
Tại đây, updateScore gói các lượt ghi trong withMutableSnapshot và điều này giúp chúng ta có được tính nhất quán trong các lượt ghi.
@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}Sau đó, trong giao diện người dùng, GameScreen truyền thẳng Trạng thái đến Tiêu đề và ScoreBadge.
@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state = viewModel.uiState
Column {
Header(state = state)
ScoreBadge(state = state)
}
}Hàm này không đọc bất kỳ thuộc tính nào, vì vậy, khi updateScore chạy, GameScreen không phải là trình đọc.
Chỉ thành phần thực tế, trong trường hợp này là ScoreBadge, bị vô hiệu hoá.
Và cuối cùng, hãy lưu ý rằng không có phần nào trong số này liên kết với giao diện người dùng.
Giao diện người dùng chỉ là một nơi mà các thuộc tính này thực sự toả sáng.
Cảm ơn bạn đã tham dự buổi trò chuyện của chúng tôi.
Đùa vậy thôi, giờ tôi sẽ nhường lời cho Elif để giới thiệu về thư viện đầu tiên, Transform.
Được rồi.
Cảm ơn Ben.
Như Ben vừa lưu ý, trạng thái tổng quan nhanh của Compose cung cấp cho chúng ta khả năng theo dõi, tính nhất quán và mức độ chi tiết tự động và linh động. Điều này khiến tôi đặt ra câu hỏi: Điều gì sẽ xảy ra nếu chúng ta cũng có thể điều khiển luồng dữ liệu bằng Ảnh chụp nhanh trạng thái Compose – Trạng thái tổng quan nhanh?
Và nếu trạng thái là mô hình dữ liệu của chúng ta, thì làm cách nào để thực sự tính toán và chuyển đổi trạng thái cho tất cả người dùng?
Đây là lúc thư viện AppState Transform mới của Jetpack phát huy tác dụng.
Trước khi tìm hiểu sâu về thư viện, chúng ta cần nói về cách thư viện ra đời.
Vì vậy, ý tưởng tận dụng công cụ Compose (là thời gian chạy và trình biên dịch ở đây) để tạo ra các giá trị trạng thái trong một thành phần không có giao diện người dùng không phải là một ý tưởng mới.
Nhiều người trong số các bạn có thể đã sử dụng hoặc đã quen thuộc với thư viện Molecule.
Và "không có giao diện người dùng" ở đây có nghĩa là không có giao diện người dùng.
Vì vậy, Molecule là thư viện tiên phong trong việc chứng minh sức mạnh của việc sử dụng Compose Runtime, và thư viện này đã cho chúng ta thấy rằng một hàm composable không nhất thiết phải phát ra các nút giao diện người dùng, mà thực sự có thể được dùng để viết hàm composable trả về một mô hình dữ liệu thuần tuý.
Trong trường hợp của Molecule, đây là Kotlin Flow hoặc StateFlow.
Điều này có nghĩa là thay vì tạo chuỗi các toán tử phản ứng như kết hợp, bạn có thể viết Kotlin thông thường.
Nhưng thay vì xử lý các luồng, điều gì sẽ xảy ra nếu mô hình dữ liệu của bạn là State, vì giao diện người dùng của bạn đã có trong Compose?
Với Transform, mô hình dữ liệu của bạn là State.
Hãy cùng xem API thực tế.
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R>Đây là toàn bộ API.
Rất đơn giản, chỉ có một hàm.
Hàm này trả về một State có thể quan sát thuần tuý, không phải Flow, không phải StateFlow.
Và bất kỳ ai sử dụng nó đều không biết hoặc không quan tâm đến việc Transform đã tạo ra nó.
Điều thực sự thú vị ở đây là khối này.
Đó là Composable.
Điều này có nghĩa là mọi thứ bạn đã biết đều hoạt động trong đó.
Hãy nhớ LaunchedEffect, collectAsState, kết quả trả về sớm, câu lệnh if đơn giản, mọi thứ bạn có thể đọc từ bên trong một thành phần kết hợp, bạn đều có thể lấy từ một phép biến đổi.
Xin lưu ý rằng tất cả API mà chúng ta thấy hôm nay trong bản trình bày này đều đang ở giai đoạn alpha và có thể thay đổi. Tuy nhiên, hãy cùng xem một ví dụ.
Hãy cùng xem xét một ứng dụng hẹn giờ tập trung rất đơn giản và nhỏ gọn.
Bạn chọn một việc, đặt hẹn giờ 25 phút, đồng hồ đếm ngược.
Mỗi khi tạo một màn hình, ngay cả khi tạo một màn hình đơn giản như thế này, bạn có thể sẽ cần kết hợp và quản lý nhiều luồng dữ liệu độc lập để tập hợp trạng thái của mình trong hệ thống.
Đối với màn hình tập trung, giả sử chúng ta có hai màn hình: phiên tập trung, tức là giai đoạn chúng ta đang thực hiện, vì vậy, có tạm dừng hay không, còn lại bao nhiêu giây và tất cả những thông tin này sẽ đến từ một công cụ hẹn giờ dưới dạng StateFlow sẽ đánh dấu, giả sử là mỗi giây.
Luồng dữ liệu còn lại sẽ là nhiệm vụ hiện tại mà người dùng đang thực hiện, được lấy từ cơ sở dữ liệu Room dưới dạng Flow.
Như vậy, chúng ta có hai luồng dữ liệu và một trạng thái màn hình ở cuối.
ViewModel sẽ là nơi chúng được kết hợp với nhau và kết quả đầu ra (chúng ta sẽ gọi là bản tóm tắt) sẽ là đoạn mã mà chúng ta sẽ viết tiếp theo.
Thông thường, điều này có nghĩa là trong ViewModel, bạn sẽ phải kết hợp các luồng này bằng cách sử dụng toán tử chuỗi.
class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: StateFlow<FocusSummary> =
combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle,
)
}Vì vậy, hãy kết hợp hai luồng với nhau thành các luồng, phân nhánh bên trong lambda, vì vậy nếu không thì ngay tại đó, và sau đó là một trạng thái ở cuối.
Với Transform, chúng ta có thể bỏ toàn bộ mã nguyên mẫu này.
class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)Lệnh gọi này có vẻ quen thuộc vì chúng ta có một giá trị ban đầu trong một phạm vi được truyền vào, nhưng không có gì ở đầu nguồn để liên kết, dẫn xuất chính là khối.
Và không giống như stateIn khi bạn có thể cần định cấu hình thời gian chờ WhileSubscribed để tạm dừng và tiếp tục các luồng ngược dòng, Transform sẽ chạy liên tục trong suốt thời gian hoạt động của phạm vi đó.
Xin nhắc lại, nguồn của chúng ta là các luồng và các luồng này được chấp nhận.
class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}Chúng ta sẽ kết nối từng luồng bằng collectAsState và phiên là StateFlow, vì vậy chúng ta không cần giá trị ban đầu, nhưng Room Flow là luồng lạnh, vì vậy chúng ta sẽ cung cấp cho nó một giá trị.
Đó là toàn bộ câu chuyện về gói thuê bao.
Thao tác đọc là thao tác đăng ký.
Đây là tính năng theo dõi tự động mà Ben đã đề cập trước đó trong ví dụ chúng ta đã thấy, nhưng trong ViewModel.
Chúng ta thậm chí có thể đưa ra các kết quả trả về sớm như thế này khi ở trạng thái rảnh.
Giả sử chúng ta không có nhiệm vụ mới, chúng ta thậm chí sẽ không xử lý phần còn lại.
Và một lần nữa, đây là tính năng theo dõi lượt đọc động mang lại hiệu quả.
Khi chúng ta ở trạng thái rảnh, Room Flow hoàn toàn không được thu thập.
Sau đó, chúng ta sẽ phát ra bản tóm tắt.
Vì vậy, nếu người dùng tạm dừng nhiệm vụ hoặc đồng hồ hẹn giờ chạy, mọi thứ sẽ tự động được cập nhật.
Vì vậy, mọi thứ sẽ phức tạp hơn một chút khi chúng ta bước ra khỏi ví dụ về một ViewModel.
Giả sử phiên này cần xuất hiện trên hai nền tảng khác, một thanh hẹn giờ nhỏ nằm ở cuối màn hình của ứng dụng trên mọi màn hình và một tiện ích trên màn hình chính.
Cả hai đều cần có cùng thông tin được lấy từ bản tóm tắt phiên, giai đoạn nào, có tạm dừng hay không, còn bao nhiêu thời gian trong đồng hồ hẹn giờ.
Điều này có nghĩa là nếu mỗi nền tảng đều tập hợp bản sao riêng, thì đó là cùng một dẫn xuất ba lần và ba vị trí khác nhau đều phải thống nhất với nhau.
Vì vậy, thay vào đó, chúng ta cần lấy bản tóm tắt một lần trong phạm vi ứng dụng, sau đó mọi thứ ở hạ nguồn sẽ sử dụng bản tóm tắt đó.
Vậy làm cách nào để triển khai một tính năng như thế này?
Trong đó, quá trình triển khai của chúng ta có thể sẽ cần 3 phần.
class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
...
}
class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
...
}
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
...
}
}
}Vì vậy, trình quản lý phiên trong phạm vi ứng dụng sẽ là nơi lấy thông tin tóm tắt, ViewModel của đồng hồ hẹn giờ nhỏ và Tiện ích xem nhanh, cả hai đều sẽ sử dụng thông tin tóm tắt được lấy trong trình quản lý phiên tập trung này.
Hãy cùng xây dựng từng thành phần theo cách truyền thống, sau đó dùng Transform.
Vì vậy, theo cách thông thường, chúng ta sẽ bắt đầu bằng cách lấy bản tóm tắt một lần trong phạm vi ứng dụng dưới dạng StateFlow.
class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: StateFlow<FocusSummary> =
combine(
sessionFlow,
taskRepository.tasksFlow
) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}Đây là các lệnh gọi kết hợp và stateIn giống như chúng ta đã thấy trước đó trong ví dụ về ViewModel, nhưng chỉ được chuyển lên một cấp.
Với Transform, chúng ta cũng có thể sử dụng lại cùng một nội dung khai báo từ ViewModel mà chúng ta đã thấy trước đó.
class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
) {
val session by sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow
.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}Điểm thay đổi duy nhất là giờ đây, nó nằm trong phạm vi ứng dụng.
Tiếp theo, chúng ta có ViewModel của đồng hồ hẹn giờ nhỏ.
class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: StateFlow<MiniTimerUiState> =
combine(
focusManager.summary,
isExpanded
) { summary, expanded ->
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MiniTimerUiState()
)
fun toggleExpanded() { isExpanded.update { !it } }
}Giả sử chúng ta cũng muốn tự đặt ra một chút thử thách ở đây.
Giả sử trong đồng hồ hẹn giờ nhỏ, ngoài hai luồng dữ liệu mà chúng ta đã có là StateFlow và Luồng phòng, chúng ta cũng muốn theo dõi xem thanh đồng hồ hẹn giờ có được mở rộng hay không.
Và chúng ta sử dụng MutableStateFlow cho việc đó.
Để làm việc này, chúng ta sẽ phải sử dụng một hàm kết hợp khác để theo dõi luồng tóm tắt và isExpanded StateFlow.
Và chúng ta có trạng thái thứ hai.
Với Transform, chúng ta không cần phải sử dụng lại bất kỳ toán tử chuỗi nào trong số này để đồng bộ hoá giữa các luồng dữ liệu.
class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: State<MiniTimerUiState> =
transform(
initialValue = MiniTimerUiState(),
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val summary by focusManager.summary
val expanded by isExpanded.collectAsState()
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}
fun toggleExpanded() { isExpanded.update { !it } }
}Điều chúng ta có thể làm, và điều này thực sự thú vị, là chúng ta có thể thực hiện các phép biến đổi lồng nhau.
Vì vậy, tại đây, ViewModel sẽ đọc bản tóm tắt (đầu ra của phép biến đổi ngược dòng) giống như mọi trạng thái khác, sau đó thu thập isExpanded bằng collectAsState.
Vì vậy, chúng ta có thể kết hợp trạng thái ngược dòng và luồng xuôi dòng trong StateFlow ngay trong cùng một khối.
Và hãy nhớ đến mức độ chi tiết.
Vì vậy, nếu bạn bật/tắt isExpanded, thì thao tác này sẽ chỉ chạy lại khối này.
Khối cấp ứng dụng và tiện ích thậm chí không cần phải nhận biết.
Cuối cùng, chúng ta có tiện ích.
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
.collectAsState()
GlanceTheme { FocusWidgetContent(summary) }
}
}
}Vì Glance là Compose, nên chúng ta có thể thu thập StateFlow bằng collectAsState bên trong provideContent như mọi thành phần kết hợp khác.
Với Transform, việc này thậm chí còn đơn giản hơn.
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
GlanceTheme { FocusWidgetContent(summary) }
}
}
}Trình quản lý tiêu điểm đang cung cấp cho chúng ta một Trạng thái Compose, vì vậy, chúng ta không cần chuyển đổi gì ở đây.
Và cuối cùng, thanh hẹn giờ nhỏ và tiện ích trên màn hình chính không bao giờ có thể không đồng nhất vì chúng đang đọc cùng một trạng thái.
Không chỉ vậy, mọi thay đổi đối với trạng thái đó sẽ được phản ánh cho tất cả người dùng trạng thái đó.
Điều đó có nghĩa là gì?
Vì vậy, nếu bạn nhấn nút tạm dừng trên tiện ích ở đây, thì thao tác này sẽ đi qua trình quản lý vào luồng phiên, quay lại thông qua bản tóm tắt và cả ba màn hình này sẽ được cập nhật đồng bộ.
Bây giờ, sau khi đã tìm hiểu một chút về cách sử dụng Transform, hãy cùng tìm hiểu sâu hơn về cơ chế hoạt động của công cụ này.
Transform API mà chúng ta đã thấy trước đó thực chất là một hàm trợ giúp dùng để tạo một Transform cơ bản được quản lý.
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R> {
val transformScope = TransformScope()
val transform = transformScope
.launchTransform(initialValue, onUpdate)
scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
) {
try { transformScope.run() } finally {
transform.dispose()
}
}
return transform
}Trên thực tế, hàm này sử dụng một TransformScope và một hàm mở rộng launchTransform để khởi chạy phép biến đổi Composable trong TransformScope này và trả về một trạng thái có thể loại bỏ.
Khi đọc từ trên xuống dưới, bạn sẽ thấy rằng lớp này làm mọi thứ cho bạn.
Hàm này tạo TransformScope và chạy Transform vào đó, đây sẽ là trạng thái mà bạn nhận lại.
Sau đó, nó cũng khởi chạy chạy trong TransformScope trên trình điều phối mà bạn cung cấp, để thời gian hoạt động của phạm vi và luồng thuộc về bạn.
Sau đó, cuối cùng sẽ loại bỏ phép biến đổi khi phạm vi kết thúc.
Đây là toàn bộ câu chuyện về vòng đời của Transform.
Vậy nên, hãy cùng xem nhanh giao diện của TransformScope tại đây.
public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
public abstract class TransformScope {
public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}
public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>
public interface DisposableState<T> : State<T> {
public fun dispose()
}Chúng ta có thể thấy rằng nó có một hàm createTransform, hàm này sẽ tạo một Biến đổi được đính kèm vào TransformScope này.
Lớp này cũng có một hàm chạy tạm ngưng, giúp khởi động TransformScope và bắt đầu quá trình cập nhật các phép biến đổi trạng thái hiện tại.
Điều đó có nghĩa là gì?
Vì vậy, lệnh chạy thực sự thiết lập Recomposer và cấu trúc.
Và mọi phép biến đổi trong phạm vi đều nằm trong thành phần đó và có phạm vi kết hợp lại riêng.
Đây là lý do chúng tôi muốn chia sẻ phạm vi và lý do chúng tôi có TransformScope.
Do đó, bất cứ khi nào trạng thái thay đổi, chỉ những Biến đổi đọc trạng thái đó mới được chạy lại.
Như đã lưu ý trước đó, run là một hàm tạm ngưng.
override suspend fun run() {
val context = currentCoroutineContext()
val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
...
val recomposer = Recomposer(context)
val composition = Composition(UnitApplier, recomposer)
try {
withContext(clock) {
if (clock is HeadlessTransformClock) {
launch(start = CoroutineStart.UNDISPATCHED) {
clock.runClock()
}
}
...
}
} finally {
// dispose of composition, cancel the clock, close the recomposer
}
}Vì vậy, hàm này sẽ chạy trên bất kỳ coroutine nào mà bạn gọi cho đến khi coroutine đó bị huỷ.
Bây giờ, bạn có thể thắc mắc rằng Recomposer cần một đồng hồ khung hình để cho biết thời điểm cần tạo khung hình.
Trong Giao diện người dùng Compose, đó là màn hình, nhưng như đã đề cập trước đó, chúng ta đang ở trong một môi trường không có giao diện người dùng, vì vậy, không có giao diện người dùng.
Vì vậy, trừ phi bạn cung cấp đồng hồ cho chúng tôi, chúng tôi sẽ tạo đồng hồ riêng.
Bằng cách tách biệt quá trình kết hợp lại khỏi đồng hồ khung hiển thị, Transform sẽ chạy tất cả các phép biến đổi và dẫn xuất trạng thái ngoài luồng giao diện người dùng.
Để làm được điều này, chúng tôi đã xây dựng một HeadlessTransformClock chuyên biệt.
Biến đổi đồng hồ khung.
Vì một tiện ích, một dịch vụ hoặc một Biến đổi khác ở hạ nguồn có thể đang chờ một giá trị từ một Biến đổi, nên đồng hồ của chúng tôi rất háo hức.
Điều này có nghĩa là ngay khi bất kỳ trạng thái nào mà một Biến đổi đang sử dụng thay đổi, một khung hình mới sẽ được tạo ngay lập tức.
Đây là một máy trạng thái nhỏ không khoá, vì vậy, việc yêu cầu một khung hình là một thao tác so sánh và đặt đơn giản.
Và khi không có gì thay đổi trong trạng thái, đồng hồ sẽ dừng lại.
Vì vậy, một phép biến đổi sử dụng trạng thái không thay đổi sẽ không tốn chi phí.
Trước khi tôi chuyển lại cho Ben, chúng ta cũng nên lưu ý rằng Transform hiện hỗ trợ rememberSaveable để đảm bảo dữ liệu của bạn được lưu trong các thay đổi về cấu hình.
Thư viện này thực hiện việc này thông qua API SavedStateContainer mới, hiện là thành phần nền tảng mà SaveableStateRegistry, SavedStateHandle và SavedStateRegistry của Compose đều được xây dựng dựa trên đó.
Bạn có thể chuyển bất kỳ thành phần nào trong số này thành SavedStateContainer hoặc tạo vùng chứa con và truyền chúng vào Transform để đảm bảo dữ liệu của bạn được lưu.
Cuối cùng, xin nhắc lại rằng tất cả API mà chúng ta đang nói đến hôm nay đều đang ở giai đoạn alpha, vì vậy, chúng có thể thay đổi.
Bây giờ, tôi xin nhường lời lại cho Ben.
Được rồi.
Cảm ơn Elif.
Giờ đây, khi đã thấy được sức mạnh của trạng thái Compose và cách Transform cho phép chúng ta lấy trạng thái đó từ bên ngoài giao diện người dùng, bạn có thể nhận thấy một điều trong ví dụ cho đến nay là dữ liệu đầu vào của chúng ta là các Luồng mà chúng ta đã kết nối bằng collectAsState.
Nhưng điều đó có luôn cần thiết không?
Tại sao không sử dụng trực tiếp trạng thái Compose trong phần tử giữ trạng thái?
Vì vậy, để trả lời câu hỏi đó, tôi thấy việc xem xét trạng thái trong ba danh mục lớn là rất hữu ích.
Đầu tiên là trạng thái trực tiếp chỉ được lưu trữ trong bộ nhớ.
Hãy nghĩ đến những thông tin như trạng thái kết nối mạng hoặc Bluetooth.
Bạn không nên sử dụng giá trị cố định.
Thứ hai là trạng thái tạm thời, trạng thái này sẽ tồn tại sau khi quá trình bị buộc tắt nhưng không cần lưu trữ vô thời hạn.
Hãy nghĩ đến những trạng thái như trạng thái giao diện người dùng (chẳng hạn như thẻ có được mở rộng hay không) và trạng thái phiên (chẳng hạn như ngăn xếp lui điều hướng hoặc bản nháp biểu mẫu).
Và thứ ba là trạng thái vĩnh viễn, được lưu trữ vô thời hạn.
Hãy nghĩ đến những thứ như lựa chọn ưu tiên của người dùng hoặc thư viện nội dung nghe nhìn trong cơ sở dữ liệu.
Vì vậy, hãy bắt đầu với cách bạn có thể sử dụng trạng thái Compose cho cả trạng thái trực tiếp và tạm thời.
Cho đến nay, hướng dẫn về cấu trúc của chúng tôi vẫn giữ trạng thái Compose trong lớp giao diện người dùng hoặc đôi khi là trong ViewModel và chúng tôi đã hướng dẫn bạn sử dụng Flow ở mọi nơi khác.
Nhưng tại sao lại như vậy?
Đó là vì ngay khi cần kết hợp một thuộc tính trạng thái với một Flow, bạn phải chuyển đổi thuộc tính đó trở lại bằng snapshotFlow.
Nhưng với Transform, bạn không cần phải chuyển đổi.
Hãy cùng xem điều này có nghĩa là gì, bắt đầu từ ViewModel.
@HiltViewModel
class TaskListViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
private val networkMonitor: NetworkMonitor,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
...
}Đối với dữ liệu đầu vào của màn hình cục bộ, chẳng hạn như selectedTaskIds tại đây, bạn có thể khai báo thuộc tính mutableStateOf thuần tuý thay vì MutableStateFlow.
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
}Và vì đây là thứ mà bạn không nhất thiết phải duy trì trong suốt quá trình bị buộc tắt, nên việc chỉ sử dụng mutableStateOf là hoàn toàn ổn.
Đối với những nội dung mà bạn muốn giữ lại sau khi bị buộc tắt, chẳng hạn như cụm từ tìm kiếm, bạn có thể sao lưu trực tiếp trạng thái có thể thay đổi của nội dung đó bằng savedStateHandle.saveable.
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
}Sau đó, giống như updateScore trong ví dụ về trò chơi trước đó, khi một phương thức cập nhật nhiều giá trị trạng thái cùng một lúc, việc gói các lượt ghi trong withMutableSnapshot sẽ đảm bảo tính nhất quán.
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
fun updateSearchQuery(query: String) {
Snapshot.withMutableSnapshot {
searchQuery = query
selectedTaskIds = emptySet()
}
}
}Sau đó, để kết hợp searchQuery với Room Flow, bạn chỉ cần đọc trực tiếp bên trong Transform trong khi thu thập Flow.
val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}Vậy còn trạng thái trực tiếp như kết nối mạng thì sao?
@Singleton
class NetworkMonitor @Inject constructor(
@ApplicationContext context: Context,
) {
private val connectivityManager =
context.getSystemService(ConnectivityManager::class.java)
...
}Dữ liệu này thường nằm trong một Singleton vì được đọc trên nhiều màn hình hoặc thậm chí là bởi nhiều thành phần ứng dụng, chẳng hạn như dịch vụ.
Tuy nhiên, điều đó không có nghĩa là bạn không thể sử dụng Trạng thái Compose để lưu trữ.
Trên thực tế, phiên bản này thường đơn giản hơn phiên bản dựa trên luồng.
Bạn không cần quy trình gọi lại hoặc bất kỳ thao tác nào để điều chỉnh, bạn chỉ cần cập nhật giá trị dấu chấm trực tiếp trong lệnh gọi lại.
@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
}Và nếu người dùng vẫn cần một luồng, bạn có thể sử dụng snapshotFlow để kết nối lại.
@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { _isOnline.value = true }
override fun onLost(network: Network) { _isOnline.value = false }
}
init {
connectivityManager.registerDefaultNetworkCallback(callback)
}
}Hãy cùng xem giao diện của tính năng này trong ViewModel.
Bạn có thể đọc isOnline trực tiếp trong một Biến đổi và biến đổi này sẽ kết hợp lại khi giá trị thay đổi.
val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
if (!networkMonitor.isOnline.value) {
return@transform TaskListState.Offline
}
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}Và tất cả những điều này chỉ diễn ra trực tiếp, không có phạm vi ứng dụng hoặc phạm vi coroutine mà bạn cần cung cấp để cập nhật trạng thái, đó chỉ là một bản cập nhật trực tiếp, đồng bộ.
Vì vậy, với Transform, ViewModel của bạn có thể kết hợp Trạng thái từ trình quản lý Singleton, Trạng thái trong ViewModel và Luồng từ Kho lưu trữ, tất cả trong một khối Transform duy nhất.
Bây giờ, hãy quay lại trạng thái tạm thời.
Trước đó, tôi đã đề cập đến hai loại: trạng thái giao diện người dùng và trạng thái phiên.
Đối với trạng thái giao diện người dùng và thậm chí là trạng thái phiên hoạt động trong một Hoạt động, ViewModel và SavedState hoạt động rất hiệu quả.
Đó chính xác là mục đích của các tính năng này.
Tuy nhiên, vấn đề sẽ phát sinh khi trạng thái phiên đó cần được chia sẻ trên nhiều thành phần giao diện người dùng và ứng dụng khác nhau.
Khi trạng thái đó di chuyển ra ngoài ViewModel, các API và hướng dẫn hiện có của chúng tôi sẽ có một chút khoảng trống.
Vì vậy, trước tiên, các API SavedState sẽ lưu vào Gói của một Hoạt động, nhưng điều đó không có sẵn trong các thành phần khác của ứng dụng, chẳng hạn như một dịch vụ.
Thứ hai, mutableStateOf trong một Singleton chỉ tồn tại trong bộ nhớ, nên không thể tồn tại sau khi bị buộc tắt.
Và đó là khoảng trống mà Jetpack AppState, StateStore lấp đầy.
Đây là một bản đồ an toàn về kiểu của Trạng thái Compose có thể được chia sẻ giữa các thành phần ứng dụng và được DataStore hỗ trợ để cung cấp, nhằm duy trì hoạt động khi bị buộc tắt.
Bạn có thể tự kết nối DataStore vào mọi Singleton, nhưng điều đó có nghĩa là bạn phải viết mã nguyên mẫu lưu và khôi phục cho mọi thuộc tính.
Với tính năng Tích hợp StateStore DataStore, bạn chỉ cần kết nối DataStore một lần, sau đó chọn tham gia từng trạng thái mà bạn muốn duy trì.
Vì vậy, hãy xây dựng tính năng đó trong ứng dụng đồng hồ hẹn giờ tập trung.
Trước tiên, chúng ta sẽ bắt đầu trong bộ nhớ, sau đó thêm tính năng lưu trữ cố định DataStore.
Để chuẩn bị, chúng ta sẽ lập mô hình FocusSession dưới dạng giao diện kín với các trạng thái rảnh, đang chạy và tạm dừng.
sealed interface FocusSession {
data object Idle : FocusSession
data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}Sau đó, để lưu trữ trạng thái này trong StateStore, chúng ta đã xác định một StateStoreKey có loại FocusSession và giá trị mặc định là rảnh.
sealed interface FocusSession { ... }
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)Sau đó, chúng ta cung cấp một Singleton StateStore trong Hilt mà chúng ta có thể chèn vào các thành phần khác nhau khi cần, chẳng hạn như Trình quản lý phiên tập trung.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(): StateStore = StateStore()
}
@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
...
}Vì vậy, tương tự như cách ViewModel chèn SavedStateHandle.
Được rồi.
Vậy là giờ chúng ta đã có StateStore, nhưng làm cách nào để sử dụng?
Thật may là API này khá nhỏ.
Lớp này chỉ có 3 phương thức, vì vậy chúng ta sẽ lần lượt tìm hiểu từng phương thức.
Đầu tiên là setState, ghi một giá trị mới cho khoá.
fun startFocus(taskId: String, durationMinutes: Int = 25) {
val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
stateStore.setState(
FocusSessionKey,
FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
)
}
fun endFocus() {
stateStore.setState(FocusSessionKey, FocusSession.Idle)
}Khi một phiên bắt đầu, chúng ta sẽ đặt phiên đó thành đang chạy và khi phiên kết thúc, chúng ta sẽ đặt phiên đó thành rảnh.
Vậy là xong.
Thứ hai, chúng ta vẫn có updateState, cho phép bạn tính toán một giá trị mới dựa trên giá trị hiện tại, chẳng hạn như tính thời gian còn lại khi chuyển đổi giữa trạng thái đang chạy và tạm dừng.
fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Running) return@updateState session
FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
}
}
fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Paused) return@updateState session
FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
}
}Và cuối cùng là getState, trả về một Trạng thái chỉ có thể đọc cho một khoá và có tuỳ chọn ghi đè giá trị mặc định, nếu bạn muốn thay đổi giá trị đó tại một trang web gọi cụ thể.
@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
...
}Ngoài ra, việc hiển thị Trạng thái FocusSession trong trình quản lý này có nghĩa là người dùng không bao giờ phải truy cập trực tiếp vào StateStore hoặc các khoá của trình quản lý này.
Bạn có thể duy trì tính đóng gói đọc và ghi.
Sau đó, thay vì Consumer, họ chỉ cần chèn Focus Session Manager, có thể họ đã có sẵn vì họ cần kiểm soát phiên, sau đó họ có thể đọc State.
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
fun focusSessionManager(): FocusSessionManager
}Sau đó, những trình đọc đó có thể trực tiếp hiển thị lại Trạng thái hoặc có thể sử dụng Biến đổi để tạo một loại Trạng thái mới, được cập nhật giống như chúng ta đã thấy trong ví dụ về Bộ hẹn giờ nhỏ ViewModel trước đó.
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}Sau đó, các Thành phần kết hợp giao diện người dùng có thể đọc Trạng thái đó giống như mọi Trạng thái Compose khác và kết hợp lại khi cần.
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}
@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
val session by viewModel.session
if (session !is FocusSession.Idle) {
TimerReadout(session = session)
}
}Nhưng cho đến nay, FocusSession chỉ tồn tại trong bộ nhớ.
Vì vậy, hãy xem những gì cần làm để ứng dụng không bị buộc tắt.
Vì vậy, trước tiên, chúng ta phải thực hiện một số bước thiết lập một lần bằng cách chạy syncToDataStore khi tạo kho lưu trữ.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(
@ApplicationContext context: Context,
@ApplicationScope scope: CoroutineScope,
): StateStore = StateStore().apply {
val path = context.dataStoreFile("app_state.json").absolutePath
scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
}
}Sau đó, chúng ta chọn sử dụng từng khoá mà chúng ta muốn duy trì.
@Serializable
sealed interface FocusSession {
@Serializable data object Idle : FocusSession
@Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
@Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
PersistToDataStoreTất cả những gì chúng ta phải làm là đảm bảo rằng loại này có thể chuyển đổi tuần tự và khoá triển khai giao diện PersistToDataStore.
Sau đó, bạn có thể thắc mắc, được rồi, có vẻ dễ dàng.
Vậy, nó thực sự làm gì để duy trì khoá?
Vì vậy, về cơ bản, syncToDataStore sử dụng API trình nghe của Transform, về cơ bản giống hệt như Transform, ngoại trừ việc nó không trả về bất kỳ kết quả nào, mà chỉ được dùng để quan sát và tác dụng phụ.
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener { … }
}Sau đó, trong trình nghe đó, trình nghe sẽ đọc từng khoá trong kho lưu trữ và kiểm tra xem có khoá nào là khoá PersistToDataStore hay không.
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener {
for (key in keys) {
if (key is PersistToDataStore) {
val value = getState(key).value
LaunchedEffect(key) {
// Restore saved value from dataStore on first access
}
LaunchedEffect(key, value) {
// Flush updated value to dataStore on change
}
}
}
}
}Khi đó, nó sẽ sử dụng LaunchedEffect để đồng bộ hoá các giá trị đó trở lại DataStore bất cứ khi nào chúng thay đổi.
Và điều tôi muốn chỉ ra ở đây là việc có một vùng chứa tiêu chuẩn như StateStore là yếu tố giúp chúng tôi cung cấp các chế độ tích hợp thả vào này.
DataStore chỉ là công cụ đầu tiên chúng ta xem xét.
Vì vậy, quay lại các danh mục trạng thái, giờ đây chúng ta đã có một bức tranh khá rõ ràng về cách bạn có thể sử dụng Trạng thái Compose trên toàn bộ ứng dụng của mình.
Từ mutableStateOf cho trạng thái trực tiếp, đến API SavedState cho trạng thái trình bày cục bộ trên màn hình và StateStore cho trạng thái phiên trên nhiều thành phần, và cuối cùng, chúng ta có collectAsState để tương tác với Luồng.
Nhưng thực ra, khả năng tương tác không bị giới hạn ở bất kỳ danh mục nào, bạn có thể sử dụng các trình trợ giúp như collectAsState bất cứ khi nào bạn cần kết nối các loại có thể quan sát vào Trạng thái Compose.
Nhưng đối với trạng thái vĩnh viễn, một điều khác mà chúng tôi đang xem xét là cách làm việc với Compose hoặc để DataStore và Room trực tiếp hiển thị Trạng thái Compose, nhờ đó bạn không cần phải viết cú pháp riêng để thực hiện việc đó.
Và tôi rất vui được thông báo rằng Transform, StateStore và tính năng tích hợp DataStore đều đã có trong bản alpha hôm nay. Chúng tôi cũng đã xuất bản tất cả tài liệu trên developer.android.com và có một kho lưu trữ AppState Recipes trên GitHub cho thấy các ví dụ về cách sử dụng cả StateStore và Transform.
[versions]
appstate = "1.0.0-alpha01"
...
[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }Và hãy thử dùng các tính năng này trong ứng dụng của bạn.
Gửi ý kiến phản hồi cho chúng tôi.
Ví dụ: chúng tôi đang trong giai đoạn alpha, vì vậy, đây là thời điểm chúng tôi thực sự có thể lặp lại và thực hiện các thay đổi để đảm bảo rằng chúng phù hợp với các trường hợp sử dụng của bạn.
/**
* A value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value.
*/
@Stable
public interface State<out T> {
public val value: T
}/**
* A mutable value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value. When the
* [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
* will be scheduled. If [value] is written to with the same value, no recompositions will be
* scheduled.
*/
@Stable
public interface MutableState<T> : State<T> {
override var value: T
public operator fun component1(): T
public operator fun component2(): (T) -> Unit
}@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
}@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}
@Composable
fun AddButton(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Add")
}
}@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(true) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(false) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Column {
Header(levelName = state.levelName)
ScoreBadge(
timeSurvived = state.timeSurvived,
score = state.score
)
}
}@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state = viewModel.uiState
Column {
Header(state = state)
ScoreBadge(state = state)
}
}public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R>class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: StateFlow<FocusSummary> =
combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle,
)
}class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
...
}
class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
...
}
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
...
}
}
}class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: StateFlow<FocusSummary> =
combine(
sessionFlow,
taskRepository.tasksFlow
) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
) {
val session by sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow
.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: StateFlow<MiniTimerUiState> =
combine(
focusManager.summary,
isExpanded
) { summary, expanded ->
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MiniTimerUiState()
)
fun toggleExpanded() { isExpanded.update { !it } }
}class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: State<MiniTimerUiState> =
transform(
initialValue = MiniTimerUiState(),
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val summary by focusManager.summary
val expanded by isExpanded.collectAsState()
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}
fun toggleExpanded() { isExpanded.update { !it } }
}class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
.collectAsState()
GlanceTheme { FocusWidgetContent(summary) }
}
}
}class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
GlanceTheme { FocusWidgetContent(summary) }
}
}
}public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R> {
val transformScope = TransformScope()
val transform = transformScope
.launchTransform(initialValue, onUpdate)
scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
) {
try { transformScope.run() } finally {
transform.dispose()
}
}
return transform
}public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
public abstract class TransformScope {
public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}
public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>
public interface DisposableState<T> : State<T> {
public fun dispose()
}override suspend fun run() {
val context = currentCoroutineContext()
val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
...
val recomposer = Recomposer(context)
val composition = Composition(UnitApplier, recomposer)
try {
withContext(clock) {
if (clock is HeadlessTransformClock) {
launch(start = CoroutineStart.UNDISPATCHED) {
clock.runClock()
}
}
...
}
} finally {
// dispose of composition, cancel the clock, close the recomposer
}
}@HiltViewModel
class TaskListViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
private val networkMonitor: NetworkMonitor,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
...
}@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
}@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
}@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
fun updateSearchQuery(query: String) {
Snapshot.withMutableSnapshot {
searchQuery = query
selectedTaskIds = emptySet()
}
}
}val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}@Singleton
class NetworkMonitor @Inject constructor(
@ApplicationContext context: Context,
) {
private val connectivityManager =
context.getSystemService(ConnectivityManager::class.java)
...
}@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
}@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { _isOnline.value = true }
override fun onLost(network: Network) { _isOnline.value = false }
}
init {
connectivityManager.registerDefaultNetworkCallback(callback)
}
}val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
if (!networkMonitor.isOnline.value) {
return@transform TaskListState.Offline
}
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}sealed interface FocusSession {
data object Idle : FocusSession
data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}sealed interface FocusSession { ... }
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(): StateStore = StateStore()
}
@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
...
}fun startFocus(taskId: String, durationMinutes: Int = 25) {
val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
stateStore.setState(
FocusSessionKey,
FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
)
}
fun endFocus() {
stateStore.setState(FocusSessionKey, FocusSession.Idle)
}fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Running) return@updateState session
FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
}
}
fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Paused) return@updateState session
FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
}
}@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
...
}@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
fun focusSessionManager(): FocusSessionManager
}@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}
@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
val session by viewModel.session
if (session !is FocusSession.Idle) {
TimerReadout(session = session)
}
}@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(
@ApplicationContext context: Context,
@ApplicationScope scope: CoroutineScope,
): StateStore = StateStore().apply {
val path = context.dataStoreFile("app_state.json").absolutePath
scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
}
}@Serializable
sealed interface FocusSession {
@Serializable data object Idle : FocusSession
@Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
@Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
PersistToDataStoresuspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener { … }
}suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener {
for (key in keys) {
if (key is PersistToDataStore) {
val value = getState(key).value
LaunchedEffect(key) {
// Restore saved value from dataStore on first access
}
LaunchedEffect(key, value) {
// Flush updated value to dataStore on change
}
}
}
}
}[versions]
appstate = "1.0.0-alpha01"
...
[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }