Nowoczesne interfejsy rzadko są statyczne. Stan interfejsu zmienia się, gdy użytkownik wchodzi z nim w interakcję lub gdy aplikacja musi wyświetlić nowe dane.
Ten dokument zawiera wytyczne dotyczące tworzenia stanu interfejsu i zarządzania nim. Po zakończeniu tego procesu:
- Wiedzieć, których interfejsów API należy użyć do utworzenia stanu interfejsu. Zależy to od charakteru źródeł zmiany stanu dostępnych w obiektach stanu, zgodnie z zasadami jednokierunkowego przepływu danych.
- Wiedzieć, jak określić zakres stanu interfejsu, aby uwzględnić zasoby systemowe.
- wiedzieć, jak udostępniać stan interfejsu do wykorzystania przez interfejs;
Zasadniczo tworzenie stanu polega na stopniowym stosowaniu tych zmian do stanu interfejsu. Stan zawsze istnieje i zmienia się w wyniku zdarzeń. Różnice między zdarzeniami a stanem zostały podsumowane w tabeli poniżej:
| Wydarzenia | Stan |
|---|---|
| są przejściowe, nieprzewidywalne i istnieją przez określony czas; | Zawsze istnieje. |
| Dane wejściowe produkcji stanowej. | Produkcja państwowa. |
| produkt interfejsu lub innych źródeł. | Jest używana przez interfejs. |
Świetnym mnemonicznym skrótem, który podsumowuje powyższe, jest stan jest; zdarzenia się zdarzają. Poniższy diagram pomaga wizualizować zmiany stanu w miarę występowania zdarzeń na osi czasu. Każde zdarzenie jest przetwarzane przez odpowiedni obiekt stanu i powoduje zmianę stanu:
Zdarzenia mogą pochodzić z tych źródeł:
- Użytkownicy: podczas korzystania z interfejsu aplikacji.
- Inne źródła zmiany stanu: interfejsy API, które prezentują dane aplikacji z interfejsu, domeny lub warstw danych, np. zdarzenia przekroczenia limitu czasu paska powiadomień, przypadki użycia lub repozytoria.
Potok produkcji stanu interfejsu
Produkcja stanu w aplikacjach na Androida może być traktowana jako potok przetwarzania, który obejmuje:
- Dane wejściowe: źródła zmiany stanu. Mogą to być:
- Lokalne w warstwie interfejsu: mogą to być zdarzenia użytkownika, np. wpisanie tytułu zadania w aplikacji do zarządzania zadaniami, lub interfejsy API, które zapewniają dostęp do logiki interfejsu, która powoduje zmiany stanu interfejsu. Na przykład wywołanie metody
opennaDrawerStatew Jetpack Compose. - Źródła zewnętrzne w stosunku do warstwy interfejsu: są to źródła z warstwy domeny lub danych, które powodują zmiany stanu interfejsu. Na przykład wiadomości, które zostały wczytane z
NewsRepository, lub inne wydarzenia. - mieszankę wszystkich powyższych.
- Lokalne w warstwie interfejsu: mogą to być zdarzenia użytkownika, np. wpisanie tytułu zadania w aplikacji do zarządzania zadaniami, lub interfejsy API, które zapewniają dostęp do logiki interfejsu, która powoduje zmiany stanu interfejsu. Na przykład wywołanie metody
- Obiekty stanu: typy, które stosują logikę biznesową lub logikę interfejsu do źródeł zmiany stanu i przetwarzają zdarzenia użytkownika, aby generować stan interfejsu.
- Dane wyjściowe: stan interfejsu, który aplikacja może renderować, aby dostarczać użytkownikom potrzebne informacje.
Interfejsy API stanu produkcji
W procesie tworzenia stanu używane są 2 główne interfejsy API, w zależności od etapu potoku:
| Etap potoku | Interfejs API |
|---|---|
| Dane wejściowe | Aby uniknąć zacinania się interfejsu, używaj asynchronicznych interfejsów API do wykonywania zadań poza wątkiem UI. Na przykład współprogramy lub przepływy w Kotlinie oraz RxJava lub wywołania zwrotne w języku programowania Java. |
| Wyniki | Gdy stan się zmieni, użyj interfejsów API do obsługi danych obserwowalnych, aby unieważnić i ponownie wyrenderować interfejs. Na przykład StateFlow, Compose State lub LiveData. Obserwowalne zmienne stanu danych gwarantują, że interfejs zawsze ma stan interfejsu do wyświetlenia na ekranie. |
Wybór asynchronicznego interfejsu API do wprowadzania danych ma większy wpływ na charakter potoku produkcji stanu niż wybór interfejsu API do obserwacji danych wyjściowych. Dzieje się tak, ponieważ dane wejściowe określają rodzaj przetwarzania, które można zastosować w przypadku potoku.
Montaż potoku produkcyjnego stanu
W kolejnych sekcjach omówimy techniki generowania stanu najlepiej dopasowane do różnych danych wejściowych oraz pasujące do nich interfejsy API danych wyjściowych. Każdy potok produkcji stanu to kombinacja danych wejściowych i wyjściowych, która powinna być:
- Świadomość cyklu życia: jeśli interfejs nie jest widoczny lub aktywny, potok produkcji stanu nie powinien zużywać żadnych zasobów, chyba że jest to wyraźnie wymagane.
- Łatwość przyswojenia: interfejs użytkownika powinien być w stanie łatwo renderować wygenerowany stan interfejsu. W przypadku różnych interfejsów View API, takich jak system View czy Jetpack Compose, kwestie dotyczące danych wyjściowych potoku produkcji stanu będą się różnić.
Dane wejściowe w potokach produkcji stanów
Dane wejściowe w potoku produkcji stanu mogą podawać źródła zmiany stanu za pomocą:
- Operacje jednorazowe, które mogą być synchroniczne lub asynchroniczne, np. wywołania funkcji
suspend. - interfejsy API strumieniowania, np.
Flows; - Wszystkie powyższe odpowiedzi
W kolejnych sekcjach opisujemy, jak utworzyć potok produkcji stanu dla każdego z powyższych danych wejściowych.
Interfejsy API wywoływane jednorazowo jako źródła zmiany stanu
Użyj interfejsu MutableStateFlow API jako obserwowalnego, modyfikowalnego kontenera stanu. W aplikacjach Jetpack Compose możesz też rozważyć użycie mutableStateOf, zwłaszcza podczas pracy z interfejsami API tekstu Compose. Oba interfejsy API oferują metody, które umożliwiają bezpieczne, niepodzielne aktualizacje przechowywanych przez nie wartości, niezależnie od tego, czy aktualizacje są synchroniczne czy asynchroniczne.
Rozważmy na przykład aktualizacje stanu w prostej aplikacji do rzucania kostką. Każdy rzut kostką przez użytkownika wywołuje synchroniczną metodę Random.nextInt(), a wynik jest zapisywany w stanie interfejsu.
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,
)
}
}
}
Stan tworzenia
@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
}
}
Zmiana stanu interfejsu z połączeń asynchronicznych
W przypadku zmian stanu, które wymagają wyniku asynchronicznego, uruchom współprogram w odpowiednim CoroutineScope. Umożliwia to odrzucenie przez aplikację pracy, gdy CoroutineScope zostanie anulowane. Następnie zmienna stanu zapisuje wynik wywołania metody zawieszania w obserwowanym interfejsie API używanym do udostępniania stanu interfejsu.
Na przykład w przykładowej architekturze znajdziesz AddEditTaskViewModel. Gdy metoda zawieszająca saveTask() zapisuje zadanie asynchronicznie, metoda update w interfejsie MutableStateFlow propaguje zmianę stanu do stanu interfejsu.
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))
}
}
}
}
}
Stan tworzenia
@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))
}
}
}
}
Zmiana stanu interfejsu z wątków w tle
W przypadku tworzenia stanu interfejsu lepiej jest uruchamiać współprogramy na głównym dyspozytorze. Czyli poza blokiem withContext w fragmentach kodu poniżej. Jeśli jednak musisz zaktualizować stan interfejsu w innym kontekście w tle, możesz to zrobić za pomocą tych interfejsów API:
- Użyj metody
withContext, aby uruchamiać korutyny w innym kontekście współbieżnym. - Podczas korzystania z
MutableStateFlowużywaj metodyupdatejak zwykle. - Podczas korzystania z Compose State używaj
Snapshot.withMutableSnapshot, aby zagwarantować niepodzielne aktualizacje stanu w kontekście współbieżnym.
Załóżmy na przykład, że we fragmencie kodu DiceRollViewModel poniżej SlowRandom.nextInt() jest wymagającą obliczeniowo funkcją suspend, która musi być wywoływana z koprogramu związanego z procesorem.
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,
)
}
}
}
}
}
Stan tworzenia
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
}
}
}
}
}
Interfejsy API strumieni jako źródła zmiany stanu
W przypadku źródeł zmiany stanu, które z czasem generują wiele wartości w strumieniach, agregowanie wyników ze wszystkich źródeł w spójną całość jest prostym podejściem do tworzenia stanu.
W przypadku Kotlin Flows możesz to osiągnąć za pomocą funkcji combine. Przykład tego rozwiązania można znaleźć w przykładzie „Now in Android" w klasie 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
)
}
Użycie operatora stateIn do utworzenia StateFlows zapewnia interfejsowi większą kontrolę nad aktywnością potoku produkcji stanu, ponieważ może on być aktywny tylko wtedy, gdy interfejs jest widoczny.
- Użyj
SharingStarted.WhileSubscribed(), jeśli potok ma być aktywny tylko wtedy, gdy interfejs jest widoczny podczas zbierania przepływu w sposób uwzględniający cykl życia. - Użyj
SharingStarted.Lazily, jeśli potok ma być aktywny tak długo, jak długo użytkownik może wrócić do interfejsu, czyli gdy interfejs znajduje się na liście wstecznej lub na innej karcie poza ekranem.
W przypadkach, w których agregowanie źródeł stanu opartych na strumieniach nie ma zastosowania, interfejsy API strumieni, takie jak Kotlin Flows, oferują bogaty zestaw przekształceń, np. scalanie, spłaszczanie itp., które pomagają w przetwarzaniu strumieni w stan interfejsu.
Interfejsy API jednorazowe i strumieniowe jako źródła zmiany stanu
W przypadku, gdy potok produkcji stanu zależy zarówno od wywołań jednorazowych, jak i od strumieni jako źródeł zmiany stanu, strumienie są ograniczeniem definiującym. Dlatego przekształć wywołania jednorazowe w interfejsy API strumieni lub przekieruj ich dane wyjściowe do strumieni i wznów przetwarzanie zgodnie z opisem w sekcji strumieni powyżej.
W przypadku przepływów zwykle oznacza to utworzenie co najmniej 1 prywatnej instancji kopii zapasowejMutableStateFlow w celu propagowania zmian stanu. Możesz też tworzyć przepływy zrzutów w stanie tworzenia.
Przyjrzyj się TaskDetailViewModel z repozytorium architecture-samples:
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 }
}
}
Stan tworzenia
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
}
}
Typy danych wyjściowych w potokach produkcji stanowej
Wybór interfejsu API wyjściowego dla stanu interfejsu i sposobu jego prezentacji zależy w dużej mierze od interfejsu API, którego aplikacja używa do renderowania interfejsu. W aplikacjach na Androida możesz używać widoków lub Jetpack Compose. Należy wziąć pod uwagę:
- Odczytywanie stanu w sposób uwzględniający cykl życia.
- Określ, czy stan powinien być udostępniany w 1 lub wielu polach z poziomu podmiotu przechowującego stan.
W tabeli poniżej znajdziesz podsumowanie interfejsów API, których należy używać w przypadku potoku produkcji stanu dla dowolnego wejścia i odbiorcy:
| Dane wejściowe | Klienci indywidualni | Wyniki |
|---|---|---|
| Interfejsy API jednorazowego użytku | Wyświetlenia | StateFlow lub LiveData |
| Interfejsy API jednorazowego użytku | Utwórz | StateFlow lub Utwórz State |
| Interfejsy Stream API | Wyświetlenia | StateFlow lub LiveData |
| Interfejsy Stream API | Utwórz | StateFlow |
| Interfejsy API jednorazowe i strumieniowe | Wyświetlenia | StateFlow lub LiveData |
| Interfejsy API jednorazowe i strumieniowe | Utwórz | StateFlow |
Inicjowanie potoku produkcyjnego stanu
Inicjowanie potoków produkcji stanu obejmuje ustawienie warunków początkowych, które umożliwiają uruchomienie potoku. Może to obejmować podanie początkowych wartości wejściowych, które są kluczowe dla rozpoczęcia potoku, np. id dla widoku szczegółowego artykułu informacyjnego lub rozpoczęcie ładowania asynchronicznego.
W miarę możliwości inicjuj potok produkcji stanu w sposób odroczony, aby oszczędzać zasoby systemu.
W praktyce często oznacza to czekanie, aż pojawi się odbiorca danych wyjściowych. Flow Interfejsy API umożliwiają to dzięki argumentowi started w metodzie stateIn. W przypadkach, w których to nie ma zastosowania, zdefiniuj idempotentną funkcję initialize(), aby jawnie uruchomić potok produkcji stanu, jak pokazano w tym fragmencie kodu:
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
}
}
}
Przykłady
Poniższe przykłady Google pokazują tworzenie stanu w warstwie interfejsu. Zapoznaj się z nimi, aby zobaczyć te wskazówki w praktyce:
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Warstwa interfejsu
- Tworzenie aplikacji działającej w trybie offline
- Obiekty stanu i stan interfejsu {:#mad-arch}