In einer Compose-Anwendung hängt es davon ab, ob die UI-Logik oder die Geschäftslogik es erfordert, wo Sie den UI-Status hochstufen. In diesem Dokument werden diese beiden Hauptszenarien beschrieben.
Best Practice
Sie sollten den UI-Status auf den niedrigsten gemeinsamen Ancestor zwischen allen Composables hochstufen, die ihn lesen und schreiben. Sie sollten den Status so nah wie möglich am Ort der Nutzung halten. Stellen Sie Nutzern über den Statusinhaber unveränderliche Status und Ereignisse zur Verfügung, um den Status zu ändern.
Der niedrigste gemeinsame Ancestor kann sich auch außerhalb der Composition befinden. Beispiel: Wenn Sie den Status in einem ViewModel hochstufen, weil Geschäftslogik involviert ist.
Auf dieser Seite werden diese Best Practice und ein wichtiger Hinweis ausführlich erläutert.
Arten von UI-Zustand und UI-Logik
Im Folgenden finden Sie Definitionen für die Arten von UI-Zuständen und -Logik, die in diesem Dokument verwendet werden.
UI-Status
Der UI-Zustand ist die Property, die die UI beschreibt. Es gibt zwei Arten von UI-Zuständen:
- Der UI-Status des Bildschirms ist das, was auf dem Bildschirm angezeigt werden soll. 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 Status von UI-Elementen bezieht sich auf Eigenschaften, die UI-Elementen inhärent sind und beeinflussen, wie sie gerendert werden. Ein UI-Element kann ein- oder ausgeblendet sein und eine bestimmte Schriftart, Schriftgröße oder Schriftfarbe haben. In Jetpack Compose ist der Zustand außerhalb der zusammensetzbaren Funktion. Sie können ihn sogar aus der unmittelbaren Umgebung der zusammensetzbaren Funktion in die aufrufende zusammensetzbare Funktion oder einen State Holder verschieben. Ein Beispiel dafür ist
ScaffoldStatefür die zusammensetzbare FunktionScaffold.
Logik
Die Logik in einer Anwendung kann entweder Geschäftslogik oder UI-Logik sein:
- Geschäftslogik ist die Implementierung von Produktanforderungen für App-Daten. Beispiel: Ein Artikel in einer Newsreader-App wird mit einem Lesezeichen versehen, wenn der Nutzer auf die Schaltfläche tippt. Diese Logik zum Speichern eines Lesezeichens in einer Datei oder Datenbank wird normalerweise in der Domain- oder Datenebene platziert. Der State Holder delegiert diese Logik normalerweise an diese Ebenen, indem er die von ihnen bereitgestellten Methoden aufruft.
- Die UI-Logik bezieht sich darauf, wie der UI-Status auf dem Bildschirm angezeigt wird. Beispiele: Abrufen des richtigen Suchleistenhinweises, wenn der Nutzer eine Kategorie ausgewählt hat, 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 auf die UI beschränken und dabei ihren Lebenszyklus berücksichtigen. Dazu sollten Sie den Status auf der richtigen Ebene in einer zusammensetzbaren Funktion hochsetzen. Alternativ können Sie dies in einer einfachen State Holder-Klasse tun, die ebenfalls auf den UI-Lebenszyklus beschränkt ist.
Im Folgenden finden Sie eine Beschreibung beider Lösungen und eine Erklärung, wann welche verwendet werden sollte.
Kompositionen als State-Inhaber
UI-Logik und UI-Elementstatus in Composables zu haben, ist ein guter Ansatz, wenn Status und Logik einfach sind. Sie können den Status innerhalb einer zusammensetzbaren Funktion belassen oder nach Bedarf hochstufen.
Kein Status-Hoisting erforderlich
Das Hochziehen des Status ist nicht immer erforderlich. Der Status kann in einer zusammensetzbaren Funktion intern gehalten werden, wenn keine andere zusammensetzbare Funktion ihn steuern muss. In diesem Snippet gibt es eine zusammensetzbare Funktion, die sich durch Tippen maximieren und minimieren lässt:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state Text( text = AnnotatedString(message.content), modifier = Modifier.clickable { showDetails = !showDetails // Apply UI logic } ) if (showDetails) { Text(message.timestamp) } }
Die Variable showDetails ist der interne Status für dieses UI-Element. Er wird nur in dieser zusammensetzbaren Funktion gelesen und geändert und die darauf angewendete Logik ist sehr einfach.
In diesem Fall würde das Hochstufen des Status also nicht viel bringen, sodass Sie ihn intern belassen können. Dadurch wird diese zusammensetzbare Funktion zum Inhaber und zur einzigen verlässlichen Quelle des maximierten Zustands.
Hoisting in Composables
Wenn Sie den Status Ihres UI-Elements für andere zusammensetzbare Funktionen freigeben und an verschiedenen Stellen UI-Logik darauf anwenden müssen, können Sie ihn in der UI-Hierarchie nach oben verschieben. Außerdem werden Ihre Composables dadurch wiederverwendbarer und leichter zu testen.
Das folgende Beispiel ist eine Chat-App, die zwei Funktionen implementiert:
- Mit der Schaltfläche
JumpToBottomscrollen Sie in der Nachrichtenliste nach unten. Die Schaltfläche führt die UI-Logik für den Listenstatus aus. - Die Liste
MessagesListwird nach unten gescrollt, nachdem der Nutzer neue Nachrichten gesendet hat. „UserInput“ führt die UI-Logik für den Listenstatus aus.
JumpToBottom-Button und Scrollen nach unten bei neuen NachrichtenDie Hierarchie der Compose-Funktionen sieht so aus:

Der Status LazyColumn wird auf den Unterhaltungsbildschirm hochgezogen, damit die App die UI-Logik ausführen und den Status aus allen erforderlichen Composables lesen kann:
LazyColumn von LazyColumn in ConversationScreen hochziehenDie Composables sind:
LazyListState, hochgestuft auf ConversationScreenDer Code sieht so aus:
@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 wie nötig für die anzuwendende UI-Logik hochgezogen. Da sie in einer zusammensetzbaren Funktion initialisiert wird, wird sie in der Composition gespeichert und folgt deren Lebenszyklus.
lazyListState ist in der Methode MessagesList mit dem Standardwert rememberLazyListState() definiert. Dies ist ein gängiges Muster in Compose.
Dadurch werden Composables wiederverwendbarer und flexibler. Sie können die zusammensetzbare Funktion dann in verschiedenen Teilen der App verwenden, in denen der Status möglicherweise nicht gesteuert werden muss. Das ist normalerweise der Fall, wenn Sie eine zusammensetzbare Funktion testen oder sich eine Vorschau davon ansehen. Genau so definiert LazyColumn seinen Status.
LazyListState ist ConversationScreenEinfache State-Holder-Klasse als State-Inhaber
Wenn eine zusammensetzbare Funktion komplexe UI-Logik enthält, die ein oder mehrere Statusfelder eines UI-Elements umfasst, sollte sie diese Aufgabe an State Holder delegieren, z. B. an eine einfache State Holder-Klasse. Dadurch lässt sich die Logik der zusammensetzbaren Funktion isoliert besser testen und die Komplexität wird reduziert. Dieser Ansatz bevorzugt das Prinzip der Trennung von Zuständigkeiten: Die zusammensetzbare Funktion ist für die Ausgabe von UI-Elementen zuständig und der State Holder enthält die UI-Logik und den Status des UI-Elements.
Einfache State Holder-Klassen bieten Aufrufern Ihrer zusammensetzbaren Funktion praktische Funktionen, sodass sie diese Logik nicht selbst schreiben müssen.
Diese einfachen Klassen werden in der Composition erstellt und gespeichert. Da sie dem Lebenszyklus des Composeables folgen, können sie von der Compose-Bibliothek bereitgestellte Typen wie rememberNavController() oder rememberLazyListState() annehmen.
Ein Beispiel hierfür ist die einfache State Holder-Klasse LazyListState, die in Compose implementiert ist, um die UI-Komplexität 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, in dem der scrollPosition für dieses UI-Element gespeichert ist. Außerdem werden Methoden zum Ändern der Scrollposition bereitgestellt, z. B. durch Scrollen zu einem bestimmten Element.
Wie Sie sehen, erhöht sich mit der Anzahl der Aufgaben einer zusammensetzbaren Funktion auch die Notwendigkeit eines State Holders. Die Verantwortlichkeiten können in der UI-Logik oder nur in der Menge des zu verfolgenden Zustands liegen.
Ein weiteres gängiges Muster ist die Verwendung einer einfachen State Holder-Klasse, um die Komplexität von zusammensetzbaren Root-Funktionen in der App zu bewältigen. Mit einer solchen Klasse können Sie den Status auf App-Ebene wie den Navigationsstatus und die Bildschirmgröße kapseln. Eine vollständige Beschreibung dazu finden Sie auf der Seite UI-Logik und ihr State Holder.
Geschäftslogik
Wenn Composables und einfache State Holder-Klassen für die UI-Logik und den Status von UI-Elementen zuständig sind, ist ein State Holder auf Bildschirmebene für die folgenden Aufgaben zuständig:
- Zugriff auf die Geschäftslogik der Anwendung, die sich normalerweise in anderen Ebenen der Hierarchie befindet, z. B. in der Geschäfts- und Datenebene.
- Anwendungsdaten für die Darstellung auf einem bestimmten Bildschirm vorbereiten, der zum UI-Status des Bildschirms wird.
ViewModels als Statusinhaber
Die Vorteile von AAC-ViewModels in der Android-Entwicklung machen sie geeignet, um Zugriff auf die Geschäftslogik zu gewähren und die Anwendungsdaten für die Darstellung auf dem Bildschirm vorzubereiten.
Wenn Sie den UI-Status in ViewModel hochstufen, verschieben Sie ihn außerhalb der Komposition.
ViewModel hochgestuft wird, wird außerhalb der Composition gespeichert.ViewModels werden nicht als Teil der Composition gespeichert. Sie werden vom Framework bereitgestellt und sind auf einen ViewModelStoreOwner beschränkt, der eine Aktivität, ein Fragment, ein Navigationsgraph oder ein Ziel eines Navigationsgraphen 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 UI-Status des Bildschirms durch die Anwendung von Geschäftsregeln erzeugt. Da der State Holder auf Bildschirmebene dafür verantwortlich ist, wird der UI-Status des Bildschirms in der Regel im State Holder auf Bildschirmebene, in diesem Fall einem ViewModel, hochgestuft.
Betrachten Sie das ConversationViewModel einer Chat-App und wie es den UI-Status des Bildschirms und die Ereignisse zu seiner Änderung verfügbar macht:
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) { /* ... */ } }
Kompositionen nutzen den in ViewModel hochgestuften UI-Status des Bildschirms. Sie sollten die ViewModel-Instanz in Ihre Composables auf Bildschirmebene einfügen, um Zugriff auf die Geschäftslogik zu ermöglichen.
Das folgende Beispiel zeigt ein ViewModel, das in einer komponierbaren Funktion auf Bildschirmebene verwendet wird.
Hier nutzt die zusammensetzbare Funktion ConversationScreen() den im ViewModel hochgestuften UI-Status des Bildschirms:
@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) /* ... */ }
Property-Aufschlüsselung
„Property Drilling“ bezieht sich auf die Weitergabe von Daten über mehrere verschachtelte untergeordnete Komponenten an den Ort, an dem sie gelesen werden.
Ein typisches Beispiel für Property Drilling in Compose ist, wenn Sie den State Holder auf Bildschirmebene auf der obersten Ebene einfügen und Status und Ereignisse an untergeordnete Composables weitergeben. Dies kann zusätzlich zu einer Überlastung der Signaturen von zusammensetzbaren Funktionen führen.
Auch wenn die Offenlegung von Ereignissen als einzelne Lambda-Parameter die Funktionssignatur überlasten könnte, wird dadurch die Sichtbarkeit der Verantwortlichkeiten der zusammensetzbaren Funktion maximiert. Sie sehen auf einen Blick, was sie tut.
Property-Drilling ist dem Erstellen von Wrapper-Klassen zum Kapseln von Status und Ereignissen an einem Ort vorzuziehen, da dadurch die Sichtbarkeit der Verantwortlichkeiten der zusammensetzbaren Funktion verringert wird. Wenn Sie keine Wrapper-Klassen haben, ist es auch wahrscheinlicher, dass Sie nur die Parameter an Composables übergeben, die sie benötigen. Das ist eine Best Practice.
Dieselbe Best Practice gilt, wenn es sich bei diesen Ereignissen um Navigationsereignisse handelt. Weitere Informationen dazu finden Sie in der Dokumentation zur Navigation.
Wenn Sie ein Leistungsproblem festgestellt haben, können Sie das Lesen des Status auch verschieben. Weitere Informationen finden Sie in der Dokumentation zur Leistung.
Status des UI-Elements
Sie können den Status von UI-Elementen auf den State Holder auf Bildschirmebene hochstufen, wenn es eine Geschäftslogik gibt, die ihn lesen oder schreiben muss.
Im Beispiel der Chat-App zeigt die App Nutzervorschläge in einem Gruppenchat an, wenn der Nutzer @ und einen Hinweis eingibt. Diese Vorschläge stammen aus der Datenebene und die Logik zur Berechnung einer Liste von Nutzervorschlägen wird als Geschäftslogik betrachtet. Die Funktion sieht so aus:
@ und einen Hinweis eingibtDie ViewModel, die diese Funktion implementiert, sieht so aus:
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, in der der Status TextField gespeichert wird. Jedes Mal, wenn der Nutzer eine neue Eingabe vornimmt, ruft die App die Geschäftslogik auf, um suggestions zu erzeugen.
suggestions ist der UI-Status des Bildschirms und wird von der Compose-UI durch Erfassen aus dem StateFlow verwendet.
Einschränkungen
Für einige Compose-UI-Elementstatus kann das Hoisting auf das ViewModel besondere Überlegungen erfordern. Einige State Holder von Compose-UI-Elementen stellen beispielsweise Methoden zum Ändern des Status bereit. Einige davon sind möglicherweise Sperrfunktionen, die Animationen auslösen. Diese Sperrfunktionen können Ausnahmen auslösen, wenn Sie sie von einem CoroutineScope aufrufen, der 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. Sie sollten den Drawer-Zustand in das ViewModel-Element hochstufen, damit Sie sowohl die UI- als auch die Geschäftslogik für dieses Element vom Zustandseigentümer aufrufen können.
Wenn Sie jedoch die Methode close() von DrawerState mit dem viewModelScope aus der Compose-UI aufrufen, wird eine Laufzeitausnahme vom Typ IllegalStateException mit der Meldung „MonotonicFrameClock ist in diesem CoroutineContext” nicht verfügbar“ ausgelöst.
Verwenden Sie stattdessen einen CoroutineScope, der auf die Composition beschränkt ist. Sie stellt eine MonotonicFrameClock in der CoroutineContext bereit, die für die Funktion der Sperrfunktionen erforderlich ist.
Um diesen Absturz zu beheben, ändere den CoroutineContext der Koroutine in der ViewModel in einen, der auf die Komposition beschränkt ist. Das 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.
Beispiele
Codelabs
Videos
Empfehlungen für Sie
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- UI-Status in Compose speichern
- Listen und Raster
- Compose-UI entwerfen