Wo Sie in einer Composer-Anwendung den UI-Status hochziehen, hängt davon ab, ob die UI-Logik oder Geschäftslogik dies erfordert. In diesem Dokument werden diese beiden Hauptszenarien erläutert.
Best Practice
Sie sollten den UI-Status zum niedrigsten gemeinsamen Ancestor aller zusammensetzbaren Funktionen, die ihn lesen und schreiben, erstellen. Sie sollten den Zustand in der Nähe des Speicherorts beibehalten, an dem er verwendet wird. Vom Inhaber des Status für Nutzer einen unveränderlichen Status und Ereignisse aufrufen, um den Status zu ändern.
Der niedrigste gemeinsame Ancestor kann sich auch außerhalb der Komposition befinden. Das ist beispielsweise der Fall, wenn ein ViewModel
-Zustand hebt, weil Geschäftslogik eine Rolle spielt.
Auf dieser Seite wird diese Best Practice ausführlich erläutert. Außerdem gibt es einige wichtige Hinweise, die Sie beachten sollten.
Typen von UI-Status und UI-Logik
Im Folgenden finden Sie Definitionen für die Typen von UI-Status und -Logik, die in diesem Dokument verwendet werden.
UI-Status
UI state ist die Eigenschaft, mit der die UI beschrieben wird. Es gibt zwei Arten von UI-Status:
- Der Bildschirm-UI-Status ist das, was auf dem Bildschirm angezeigt werden muss. Eine
NewsUiState
-Klasse kann beispielsweise die Nachrichtenartikel und andere Informationen enthalten, die zum Rendern der UI erforderlich sind. Dieser Status ist normalerweise mit anderen Ebenen der Hierarchie verbunden, da er App-Daten enthält. - Der UI-Elementstatus bezieht sich auf Eigenschaften von UI-Elementen, die ihr Rendering beeinflussen. Ein UI-Element kann ein- oder ausgeblendet sein und eine bestimmte Schriftart, Schriftgröße oder Schriftfarbe haben. In Android-Ansichten verwaltet die Ansicht diesen Status selbst, da sie von Natur aus zustandsorientiert ist. Sie stellt Methoden zum Ändern oder Abfragen des Status zur Verfügung. Ein Beispiel dafür sind die Methoden
get
undset
der KlasseTextView
für ihren Text. In Jetpack Composer befindet sich der Status außerhalb der zusammensetzbaren Funktion. Sie können sie sogar aus der unmittelbaren Nähe der zusammensetzbaren Funktion in die aufrufende zusammensetzbare Funktion oder einen Statusinhaber hochziehen. Ein Beispiel dafür istScaffoldState
für die zusammensetzbare FunktionScaffold
.
Logik
Die Logik in einer Anwendung kann entweder Geschäftslogik oder UI-Logik sein:
- Die Geschäftslogik ist die Implementierung von Produktanforderungen an Anwendungsdaten. Beispiel: Einen Artikel in einer Newsreader-App als Lesezeichen speichern, wenn der Nutzer auf die Schaltfläche tippt. Die Logik zum Speichern eines Lesezeichens in einer Datei oder Datenbank wird normalerweise in der Domain oder den Datenschichten platziert. Der Zustandsinhaber delegiert diese Logik normalerweise an diese Ebenen, indem er die von ihnen verfügbaren Methoden aufruft.
- Die UI-Logik bezieht sich darauf, wie der UI-Status auf dem Bildschirm angezeigt wird. So lässt sich beispielsweise der Hinweis auf der rechten Seite der Suchleiste anzeigen, wenn der Nutzer eine Kategorie ausgewählt hat, das Scrollen zu einem bestimmten Element in einer Liste oder die Navigationslogik zu einem bestimmten Bildschirm, wenn der Nutzer auf eine Schaltfläche klickt.
UI-Logik
Wenn die UI-Logik den Status lesen oder schreiben muss, sollten Sie den Status gemäß seinem Lebenszyklus auf die UI beschränken. Um dies zu erreichen, sollten Sie den Zustand in einer zusammensetzbaren Funktion auf die richtige Ebene heben. Alternativ können Sie dies in einer einfachen Halterklasse tun, die ebenfalls auf den UI-Lebenszyklus beschränkt ist.
Im Folgenden finden Sie eine Beschreibung der beiden Lösungen und eine Erklärung, wann Sie welche verwenden sollten.
Zusammensetzbare Funktionen als Zustandsinhaber
Die UI-Logik und der UI-Elementstatus in zusammensetzbaren Funktionen sind ein guter Ansatz, wenn Status und Logik einfach sind. Sie können Ihren Zustand intern bei Bedarf einer zusammensetzbaren Funktion oder einer Winde belassen.
Keine staatlichen Winden erforderlich
Der Windenstatus ist nicht immer erforderlich. Der Status kann intern in einer zusammensetzbaren Funktion belassen werden, wenn er von keiner anderen zusammensetzbaren Funktion gesteuert werden muss. Dieses Snippet enthält eine zusammensetzbare Funktion, die beim Tippen maximiert und minimiert wird:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } // Apply simple UI logic ) if (showDetails) { Text(message.timestamp) } }
Die Variable showDetails
ist der interne Status für dieses UI-Element. Sie wird in dieser zusammensetzbaren Funktion nur gelesen und geändert und die darauf angewendete Logik ist sehr einfach.
In diesem Fall wäre das Hochziehen des Bundesstaates daher keinen großen Nutzen, sodass Sie es intern belassen können. Dadurch wird diese zusammensetzbare Funktion zum Eigentümer und zu einer einzigen verlässlichen Quelle für den maximierten Status.
Hebezüge in zusammensetzbaren Komponenten
Wenn Sie den Zustand Ihres UI-Elements mit anderen zusammensetzbaren Funktionen teilen und die UI-Logik an verschiedenen Stellen darauf anwenden möchten, können Sie es in der UI-Hierarchie nach oben verschieben. Das macht Ihre zusammensetzbaren Funktionen außerdem leichter wiederverwendbar und einfacher zu testen.
Das folgende Beispiel zeigt eine Chat-App, die zwei Funktionen implementiert:
- Mit der Schaltfläche
JumpToBottom
wird die Nachrichtenliste ans Ende gescrollt. Die Schaltfläche führt die UI-Logik für den Listenstatus aus. - Die Liste
MessagesList
wird ans Ende der Liste gescrollt, nachdem der Nutzer neue Nachrichten gesendet hat. UserInput führt eine UI-Logik für den Listenstatus aus.
Die zusammensetzbare Hierarchie sieht so aus:
Der Status LazyColumn
wird auf den Unterhaltungsbildschirm hochgezogen, damit die App UI-Logik ausführen und den Status aus allen zusammensetzbaren Funktionen lesen kann, die ihn benötigen:
Die zusammensetzbaren Funktionen sind:
Der Code lautet wie folgt:
@Composable private fun ConversationScreen(/*...*/) { val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen MessagesList(messages, lazyListState) // Reuse same state in MessageList UserInput( onMessageSent = { // Apply UI logic to lazyListState scope.launch { lazyListState.scrollToItem(0) } }, ) } @Composable private fun MessagesList( messages: List<Message>, lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value ) { LazyColumn( state = lazyListState // Pass hoisted state to LazyColumn ) { items(messages, key = { message -> message.id }) { item -> Message(/*...*/) } } val scope = rememberCoroutineScope() JumpToBottom(onClicked = { scope.launch { lazyListState.scrollToItem(0) // UI logic being applied to lazyListState } }) }
LazyListState
wird so hoch gezogen, wie es für die anzuwendende UI-Logik erforderlich ist. Da sie in einer zusammensetzbaren Funktion initialisiert wird, wird sie gemäß ihrem Lebenszyklus in der Komposition gespeichert.
lazyListState
wird in der Methode MessagesList
mit dem Standardwert rememberLazyListState()
definiert. Dies ist ein gängiges Muster in der Funktion „Compose“.
Dadurch sind zusammensetzbare Funktionen wiederverwendbar und flexibler. Sie können die zusammensetzbare Funktion dann in verschiedenen Teilen der Anwendung verwenden, die möglicherweise den Status nicht steuern müssen. Das ist normalerweise der Fall, wenn eine zusammensetzbare Funktion getestet oder als Vorschau angezeigt wird. Genau so definiert LazyColumn
seinen Status.
Halter-Klasse als Zustandsinhaber
Wenn eine zusammensetzbare UI eine komplexe UI-Logik enthält, die ein oder mehrere Statusfelder eines UI-Elements umfasst, sollte sie diese Verantwortung wie eine einfache Statusinhaberklasse an Inhaber des Bundesstaates delegieren. Dies macht die Logik der zusammensetzbaren Funktion isolierter testbar und verringert ihre Komplexität. Dieser Ansatz begünstigt das Prinzip der Trennung von Belangen: Die zusammensetzbare Funktion gibt UI-Elemente aus, während der Statusinhaber die UI-Logik und den UI-Elementstatus enthält.
Einfache Zustands-Holder-Klassen bieten Aufrufer Ihrer zusammensetzbaren Funktion praktische Funktionen, sodass sie diese Logik nicht selbst schreiben müssen.
Diese einfachen Klassen werden erstellt und in der Komposition gespeichert. Da sie dem Lebenszyklus der zusammensetzbaren Funktion entsprechen, können sie Typen verwenden, die von der Composer-Bibliothek bereitgestellt werden, z. B. rememberNavController()
oder rememberLazyListState()
.
Ein Beispiel dafür ist die Halter-Klasse LazyListState
im einfachen Zustand, die in Compose implementiert wird, um die Komplexität der Benutzeroberfläche von LazyColumn
oder LazyRow
zu steuern.
// LazyListState.kt @Stable class LazyListState constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0 ) : ScrollableState { /** * The holder class for the current scroll position. */ private val scrollPosition = LazyListScrollPosition( firstVisibleItemIndex, firstVisibleItemScrollOffset ) suspend fun scrollToItem(/*...*/) { /*...*/ } override suspend fun scroll() { /*...*/ } suspend fun animateScrollToItem() { /*...*/ } }
LazyListState
kapselt den Status des LazyColumn
, der scrollPosition
für dieses UI-Element speichert. Außerdem sind Methoden zur Änderung der Scrollposition verfügbar, z. B. durch Scrollen zu einem bestimmten Element.
Wie Sie sehen, wird durch die Erhöhung der Verantwortlichkeiten einer zusammensetzbaren Funktion auch der Bedarf an einem Statusinhaber erhöht. Die Verantwortlichkeiten können in der UI-Logik oder einfach in der Menge der Zustände liegen, die im Auge behalten werden sollen.
Ein weiteres gängiges Muster ist die Verwendung einer einfachen Zustands-Holder-Klasse, um die Komplexität der zusammensetzbaren Stammfunktionen in der Anwendung zu verarbeiten. Sie können eine solche Klasse verwenden, um den Status auf App-Ebene wie den Navigationsstatus und die Bildschirmgröße zu kapseln. Eine vollständige Beschreibung davon finden Sie auf der UI-Logik und der Seite des Statusinhabers.
Geschäftslogik
Wenn zusammensetzbare und einfache Status-Holds-Klassen für die UI-Logik und den UI-Elementstatus verantwortlich sind, ist ein Inhaber des Status auf Bildschirmebene für die folgenden Aufgaben verantwortlich:
- Zugriff auf die Geschäftslogik der Anwendung gewähren, die sich normalerweise in anderen Hierarchieebenen befindet, z. B. auf der Geschäfts- und der Datenebene.
- Vorbereiten der Anwendungsdaten für die Darstellung auf einem bestimmten Bildschirm, der dann zum UI-Status des Bildschirms wird.
ViewModels als Zustandsinhaber
Aufgrund der Vorteile der AAC ViewModels in der Android-Entwicklung eignen sie sich gut, um Zugriff auf die Geschäftslogik zu gewähren und die Anwendungsdaten für die Darstellung auf dem Bildschirm vorzubereiten.
Wenn der UI-Zustand in ViewModel
aufgezogen wird, verschieben Sie ihn aus der Zusammensetzung.
ViewModels werden nicht als Teil der Komposition gespeichert. Sie werden vom Framework bereitgestellt und sind auf ein ViewModelStoreOwner
beschränkt, das eine Aktivität, ein Fragment, eine Navigationsgrafik oder das Ziel eines Navigationsdiagramms sein kann. Weitere Informationen zu ViewModel
-Bereichen finden Sie in der Dokumentation.
Dann ist ViewModel
die „Source of Truth“ und der niedrigste gemeinsame Ancestor für den UI-Status.
UI-Status des Bildschirms
Gemäß den obigen Definitionen wird der Status der Bildschirm-UI durch Anwenden von Geschäftsregeln erzeugt. Da der Inhaber des Bildschirmstatus dafür verantwortlich ist, bedeutet dies, dass der Bildschirm-UI-Status in der Regel auf den Statusinhaber auf Bildschirmebene gezogen wird, in diesem Fall ViewModel
.
Betrachten Sie die ConversationViewModel
einer Chat-App und wie sie den UI-Status und die Ereignisse des Bildschirms anzeigt, um ihn zu ändern:
class ConversationViewModel( channelId: String, messagesRepository: MessagesRepository ) : ViewModel() { val messages = messagesRepository .getLatestMessages(channelId) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) // Business logic fun sendMessage(message: Message) { /* ... */ } }
Zusammensetzbare Funktionen nutzen den Bildschirm-UI-Status, der im ViewModel
hochgezogen wird. Sie sollten die ViewModel
-Instanz in Ihre zusammensetzbaren Funktionen auf Bildschirmebene einfügen, um Zugriff auf die Geschäftslogik zu gewähren.
Das folgende Beispiel zeigt ein ViewModel
, das in einer zusammensetzbaren Funktion auf Bildschirmebene verwendet wird.
Hier nutzt die zusammensetzbare Funktion ConversationScreen()
den Bildschirm-UI-Status, der im ViewModel
hochgezogen wird:
@Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val messages by conversationViewModel.messages.collectAsStateWithLifecycle() ConversationScreen( messages = messages, onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) } ) } @Composable private fun ConversationScreen( messages: List<Message>, onSendMessage: (Message) -> Unit ) { MessagesList(messages, onSendMessage) /* ... */ }
Bohrungen von Immobilien
„Property-Aufschlüsselung“ bezieht sich auf die Übergabe von Daten über mehrere verschachtelte untergeordnete Komponenten an den Ort, an dem sie gelesen werden.
Ein typisches Beispiel für eine Eigenschaftsaufschlüsselung in der Funktion „Compose“ ist das Einfügen des Inhabers für den Status auf Bildschirmebene auf der obersten Ebene und das Übergeben von Status und Ereignissen an untergeordnete zusammensetzbare Funktionen. Dies kann außerdem zu einer Überlastung von zusammensetzbaren Funktionssignaturen führen.
Obwohl das Bereitstellen von Ereignissen als einzelne Lambda-Parameter die Funktionssignatur überlasten könnte, wird die Sichtbarkeit der Verantwortlichkeiten der zusammensetzbaren Funktion maximiert. Sie sehen auf einen Blick, was sie kann.
Das Aufschlüsseln von Attributen ist gegenüber dem Erstellen von Wrapper-Klassen besser, um Status und Ereignisse an einem Ort zu kapseln, da dies die Sichtbarkeit der zusammensetzbaren Verantwortlichkeiten reduziert. Wenn Sie keine Wrapper-Klassen haben, ist es außerdem wahrscheinlicher, dass zusammensetzbare Funktionen nur die Parameter übergeben, die sie benötigen, was eine Best Practice ist.
Dieselbe Best Practice gilt, wenn es sich bei diesen Ereignissen um Navigationsereignisse handelt. Weitere Informationen dazu finden Sie in der Navigationsdokumentation.
Wenn Sie ein Leistungsproblem festgestellt haben, können Sie auch das Lesen des Status auf später verschieben. Weitere Informationen finden Sie in der Dokumentation zur Leistung.
Status des UI-Elements
Sie können den Status des UI-Elements zum Statusinhaber auf Bildschirmebene hochziehen, wenn eine Geschäftslogik ihn lesen oder schreiben muss.
Ausgehend vom Beispiel einer Chat-App zeigt die App Nutzervorschläge in einem Gruppenchat an, wenn der Nutzer @
und einen Hinweis eingibt. Diese Vorschläge stammen aus der Datenschicht und die Logik zur Berechnung einer Liste von Nutzervorschlägen wird als Geschäftslogik betrachtet. Die Funktion sieht so aus:
Das ViewModel
, das diese Funktion implementiert, würde so aussehen:
class ConversationViewModel(/*...*/) : ViewModel() { // Hoisted state var inputMessage by mutableStateOf("") private set val suggestions: StateFlow<List<Suggestion>> = snapshotFlow { inputMessage } .filter { hasSocialHandleHint(it) } .mapLatest { getHandle(it) } .mapLatest { repository.getSuggestions(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) fun updateInput(newInput: String) { inputMessage = newInput } }
inputMessage
ist eine Variable, die den Status TextField
speichert. Jedes Mal, wenn der Nutzer eine neue Eingabe eingibt, ruft die Anwendung die Geschäftslogik auf, um suggestions
zu generieren.
suggestions
ist der Status der Bildschirm-UI. Er wird über die UI zum Verfassen aus der StateFlow
abgerufen.
Vorbehalt
Bei einigen UI-Elementstatus „Compose“ sind beim Hochziehen zum ViewModel
möglicherweise besondere Überlegungen erforderlich. Beispielsweise stellen einige Statusinhaber von UI-Elementen zum Erstellen Methoden Methoden zum Ändern des Status zur Verfügung. z. B. zum Sperren von
Funktionen, die Animationen auslösen. Diese Sperrfunktionen können Ausnahmen ausgeben, wenn Sie sie aus einer CoroutineScope
aufrufen, die nicht auf die Komposition beschränkt ist.
Angenommen, der Inhalt der App-Leiste ist dynamisch und Sie müssen ihn nach dem Schließen aus der Datenschicht abrufen und aktualisieren. Du solltest den Schubladenstatus zum ViewModel
hochziehen, damit du sowohl die UI als auch die Geschäftslogik für dieses Element vom Staatsinhaber aufrufen kannst.
Wenn Sie jedoch die Methode close()
von DrawerState
mit viewModelScope
über die Benutzeroberfläche zum Schreiben aufrufen, wird eine Laufzeitausnahme vom Typ IllegalStateException
mit der Meldung „a MonotonicFrameClock
ist hier nicht verfügbar CoroutineContext”
verursacht.
Verwenden Sie einen CoroutineScope
, der der Zusammensetzung zugeordnet ist, um dieses Problem zu beheben. Es stellt ein MonotonicFrameClock
im CoroutineContext
bereit, das für das Funktionieren der Sperrungsfunktionen erforderlich ist.
Um diesen Absturz zu beheben, ändern Sie den CoroutineContext
der Koroutine im ViewModel
in einen Wert, der der Zusammensetzung zugeordnet ist. Es könnte so aussehen:
class ConversationViewModel(/*...*/) : ViewModel() { val drawerState = DrawerState(initialValue = DrawerValue.Closed) private val _drawerContent = MutableStateFlow(DrawerContent.Empty) val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow() fun closeDrawer(uiScope: CoroutineScope) { viewModelScope.launch { withContext(uiScope.coroutineContext) { // Use instead of the default context drawerState.close() } // Fetch drawer content and update state _drawerContent.update { content } } } } // in Compose @Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val scope = rememberCoroutineScope() ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) }) }
Weitere Informationen
Weitere Informationen zu State und Jetpack Compose finden Sie in den folgenden zusätzlichen Ressourcen.
Produktproben
Codelabs
Videos
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- UI-Status in „Compose“ speichern
- Listen und Raster
- Benutzeroberfläche zum Schreiben einer Nachricht erstellen