Wo soll die Winden festgehalten werden?

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 und set der Klasse TextView 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 ist ScaffoldState für die zusammensetzbare Funktion Scaffold.

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.
Chat-App mit JumpToBottom-Schaltfläche, um bei neuen Nachrichten nach unten zu scrollen
Abbildung 1. Chat-App mit der Schaltfläche „JumpToBottom“, die bei neuen Nachrichten nach unten scrollt

Die zusammensetzbare Hierarchie sieht so aus:

Zusammensetzbare Baumstruktur für Chat
Abbildung 2: Zusammensetzbare Struktur für Chat

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:

LazyColumn-Status von LazyColumn zum ConversationScreen hochziehen
Abbildung 3: LazyColumn-Zustand von LazyColumn in ConversationScreen aufnehmen

Die zusammensetzbaren Funktionen sind:

Zusammensetzbare Chat-Baumstruktur mit LazyListState, das in ConversationScreen hochgestuft wurde
Abbildung 4: Chat zusammensetzbarer Baum mit LazyListState hochgezogen nach ConversationScreen

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.

Niedrigster gemeinsamer Ancestor für LazyListState ist ConversationScreen
Abbildung 5: Niedrigster gemeinsamer Vorgänger für LazyListState ist ConversationScreen

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.

Der auf ViewModel erhobene Status wird außerhalb der Komposition gespeichert.
Abbildung 6: Der zum ViewModel gezogene Status wird außerhalb der Komposition gespeichert.

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:

Funktion, die in einem Gruppenchat Vorschläge von Nutzern anzeigt, wenn der Nutzer „@“ und einen Hinweis eingibt
Abbildung 7: Funktion, die in einem Gruppenchat Vorschläge von Nutzern anzeigt, wenn der Nutzer @ und einen Hinweis eingibt

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