Wo soll die Winden festgehalten werden?

Wenn Sie in einer Compose-Anwendung den UI-Status hochheben, hängt davon ab, ob die Benutzeroberfläche oder Geschäftslogik erfordert. In diesem Dokument werden diese beiden Szenarien durchführen.

Best Practice

Sie sollten den UI-Status auf den niedrigsten gemeinsamen Ancestor zwischen allen die die Daten lesen und schreiben. Bundesstaat sollte möglichst nahe am Standort liegen verbraucht. Vom Inhaber des Bundesstaats für Nutzer unveränderliche Status und Ereignisse sichtbar machen um den Status zu ändern.

Der niedrigste gemeinsame Ancestor kann auch außerhalb der Komposition liegen. Beispiel: beim Hochziehen des Zustands in einem ViewModel, da die Geschäftslogik beteiligt ist.

Auf dieser Seite wird diese bewährte Vorgehensweise ausführlich erläutert und es ist ein wichtiger Punkt zu beachten.

Arten von UI-Status und UI-Logik

Unten finden Sie Definitionen für UI-Status- und Logiktypen, die verwendet werden in diesem Dokument.

UI-Status

Der UI-Status ist die Eigenschaft, die die UI beschreibt. Es gibt zwei Arten von UI-Elementen. Bundesland:

  • Der Bildschirm-UI-Status gibt an, was auf dem Bildschirm angezeigt werden muss. Beispiel: NewsUiState-Kurs kann die Nachrichtenartikel und andere benötigte Informationen enthalten. um die UI zu rendern. Dieser Zustand ist normalerweise mit anderen Schichten des da sie App-Daten enthält.
  • Der UI-Elementstatus bezieht sich auf Eigenschaften, die UI-Elementen inhärent sind und die wie sie gerendert werden. Ein UI-Element kann ein- oder ausgeblendet sein und eine bestimmte Schriftart, Schriftgröße oder -farbe haben. In Android Views kann die Ansicht verwaltet diesen Zustand selbst, da er inhärent zustandsorientiert ist und Methoden offenlegt, ändern oder abfragen können. Ein Beispiel hierfür sind get und set-Methoden der Klasse TextView für den Text. Im Jetpack Compose-Objekt erstellen, liegt der Status außerhalb der zusammensetzbaren Funktion und Sie können sie sogar winden. aus der unmittelbaren Nähe der zusammensetzbaren Funktion in die aufrufende für eine Funktion oder einen Staatsinhaber sein. Ein Beispiel hierfür ist ScaffoldState für den Scaffold zusammensetzbar.

Logik

Die Logik in einer Anwendung kann entweder Geschäftslogik oder UI-Logik sein:

  • Die Geschäftslogik ist die Implementierung von Produktanforderungen für Apps. Daten. Sie können beispielsweise einen Artikel in einer Newsreader-App als Lesezeichen speichern, tippt auf die Schaltfläche. Diese Logik zum Speichern eines Lesezeichens in einer Datei oder Datenbank normalerweise in den Domänen- oder Datenebenen platziert. Der staatliche Inhaber ist in der Regel delegiert diese Logik an diese Layer, indem die von ihnen bereitgestellten Methoden aufgerufen werden.
  • UI-Logik bezieht sich darauf, wie der UI-Status auf dem Bildschirm angezeigt wird. Für Beispiel: Der richtige Suchleistenhinweis wird angezeigt, wenn der Nutzer eine Kategorie, das Scrollen zu einem bestimmten Element in einer Liste oder die Navigationslogik wenn Nutzende auf eine Schaltfläche klicken.

UI-Logik

Wenn die UI-Logik den Status lesen oder schreiben muss, sollten Sie den Status gemäß seinem Lebenszyklus der UI zuordnen. Um dies zu erreichen, sollten Sie den Zustand in einer zusammensetzbaren Funktion auf der richtigen Ebene heben. Alternativ können Sie tun Sie dies in einer einfachen State Holder-Klasse, die sich auch auf den UI-Lebenszyklus bezieht.

Im Folgenden finden Sie eine Beschreibung der beiden Lösungen und eine Erklärung, wann welche eingesetzt werden sollten.

Zusammensetzbare Informationen als State Owner

Die Angabe von UI-Logik und UI-Elementstatus in zusammensetzbaren Funktionen ist ein guter Ansatz, Zustand und Logik einfach ist. Sie können den Status intern auf eine zusammensetzbare oder Winde nach Bedarf.

Kein Hebevorgang erforderlich

Der Status des Windes ist nicht immer erforderlich. Der Status kann intern in einer zusammensetzbaren Funktion beibehalten werden. wenn sie von keiner anderen zusammensetzbaren Funktion gesteuert werden muss. Dieses Snippet enthält eine zusammensetzbare Funktion, die durch 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. Es ist nur die in dieser zusammensetzbaren Funktion gelesen und geändert wird. Die darauf angewendete Logik ist sehr einfach. Das Hochziehen des Bundesstaats in diesem Fall braucht daher keinen großen Nutzen. intern lassen. Dadurch wird die zusammensetzbare Funktion zu „Owner“ und „Single“ und die Informationsquelle des erweiterten Zustands.

Heben in zusammensetzbaren Materialien

Wenn Sie den Status Ihres UI-Elements mit anderen zusammensetzbaren Funktionen teilen und die UI anwenden müssen an verschiedenen Stellen hinzufügen, können Sie sie in der UI-Hierarchie weiter nach oben schieben. Außerdem sind Ihre zusammensetzbaren Funktionen dadurch leichter wiederverwendbar und leichter zu testen.

Das folgende Beispiel ist eine Chat-App, die zwei Funktionen implementiert:

  • Mit der Schaltfläche JumpToBottom scrollen Sie in der Nachrichtenliste nach unten. Die Taste die Benutzeroberflächenlogik für den Listenstatus ausführt.
  • Die Liste „MessagesList“ wird nach unten gescrollt, nachdem der Nutzer neue Nachrichten gesendet hat Nachrichten. UserInput führt Benutzeroberflächenlogik für den Listenstatus aus.
<ph type="x-smartling-placeholder">
</ph> Chat-App mit der Schaltfläche „JumpToBottom“ und Scrollen bei neuen Nachrichten nach unten
Abbildung 1. Chat-App mit einer JumpToBottom-Schaltfläche und bei neuen Nachrichten nach unten scrollen

Die zusammensetzbare Hierarchie sieht so aus:

<ph type="x-smartling-placeholder">
</ph> Zusammensetzbare Struktur für Chat
Abbildung 2: Zusammensetzbare Struktur für Chat

Der Status LazyColumn wird auf den Unterhaltungsbildschirm gezogen, damit die App UI-Logik ausführen und den Status aus allen zusammensetzbaren Funktionen lesen, für die er erforderlich ist:

<ph type="x-smartling-placeholder">
</ph> LazyColumn-Status von der LazyColumn auf den ConversationScreen hochziehen
Abbildung 3: Heben von LazyColumn von LazyColumn in ConversationScreen

Die zusammensetzbaren Funktionen sind also:

<ph type="x-smartling-placeholder">
</ph> Zusammensetzbare Struktur für Chat mit LazyListState in ConversationScreen hochgeladen
Abbildung 4: Zusammensetzbarer Baum für Chat mit LazyListState auf ConversationScreen hochgezogen

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 angehoben, wie es für die UI-Logik erforderlich ist. angewendet. Da sie in einer zusammensetzbaren Funktion initialisiert wird, wird sie im Komposition nach ihrem Lebenszyklus.

lazyListState wird in der Methode MessagesList definiert, wobei der Parameter Standardwert rememberLazyListState(). Dies ist ein gängiges Muster beim Schreiben. Zusammensetzbare Funktionen sind dadurch wiederverwendbar und flexibler. Sie können dann die zusammensetzbare Funktion in verschiedenen Teilen der App, die den Status möglicherweise nicht steuern müssen. Dies ist Dies ist normalerweise der Fall, wenn Sie eine zusammensetzbare Funktion testen oder in der Vorschau ansehen. Genau so LazyColumn definiert seinen Status.

<ph type="x-smartling-placeholder">
</ph> Niedrigster gemeinsamer Ancestor für LazyListState ist ConversationScreen
Abbildung 5: Niedrigster gemeinsamer Ancestor für LazyListState ist ConversationScreen

Klasse Inhaberklasse als Inhaber eines US-Bundesstaats

Wenn eine zusammensetzbare Funktion komplexe UI-Logik enthält, die einen oder mehrere Status umfasst eines UI-Elements verwendet, sollte diese Zuständigkeit an die Statusangabe holders, z. B. eine einfache State Holder-Klasse. Dadurch wird die Logik der zusammensetzbaren Funktion besser isoliert testbar und verringert die Komplexität. Bei diesem Ansatz werden die Prinzip der Trennung von Bedenken: Die zusammensetzbare Funktion hat die Verantwortung. der Ausgabe von UI-Elementen und der Statusinhaber enthält UI-Logik und Elementstatus.

Einfache State Holder-Klassen bieten praktische Funktionen für Aufrufer Ihrer zusammensetzbaren Funktion, damit sie diese Logik nicht selbst schreiben müssen.

Diese einfachen Klassen werden in der Komposition erstellt und gespeichert. Da sie die dem Lebenszyklus der zusammensetzbaren Funktion folgen, können sie Typen annehmen, die vom Erstellen Sie eine Bibliothek, z. B. rememberNavController() oder rememberLazyListState().

Ein Beispiel hierfür ist der einfache Statusinhaber LazyListState. Klasse, implementiert in Compose zur Steuerung der UI-Komplexität von LazyColumn oder LazyRow.

// 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 von LazyColumn, in dem die scrollPosition für dieses UI-Element. Es stellt auch Methoden zum Ändern des Scrollposition by – z. B. das Scrollen zu einem bestimmten Element.

Wie Sie sehen, steigt die Erhöhung der Verantwortlichkeiten einer zusammensetzbaren Funktion für einen staatlichen Inhaber. Die Verantwortlichkeiten können in der Benutzeroberflächenlogik oder nur in die Menge des Zustands, den Sie im Auge behalten möchten.

Ein weiteres gängiges Muster ist die Verwendung einer einfachen State Holder-Klasse zur Verarbeitung der Komplexität der zusammensetzbaren Stammfunktionen in der App. Mit einer solchen Klasse können Sie der Zustand auf App-Ebene wie den Navigationsstatus und die Bildschirmgröße. Eine vollständige Eine Beschreibung dazu finden Sie auf der Seite UI-Logik und Statusinhaber.

Geschäftslogik

Wenn zusammensetzbare Funktionen und einfache Zustandsinhaberklassen für die UI-Logik und UI-Elementstatus. Ein Inhaber eines Bildschirmebenenstatus ist für Folgendes verantwortlich: Aufgaben:

  • Zugriff auf die Geschäftslogik der Anwendung bereitstellen, die normalerweise in anderen Hierarchieebenen wie den Geschäfts- und Datenebenen.
  • Vorbereiten der Anwendungsdaten für die Präsentation in einem bestimmten Bildschirm was zum UI-Status des Bildschirms wird.

ViewModels als Inhaber des Bundesstaats

Aufgrund der Vorteile von AAC ViewModels in der Android-Entwicklung für den Zugriff auf die Geschäftslogik und die Vorbereitung der Anwendungsdaten für die Präsentation auf dem Bildschirm.

Wenn du den UI-Status in der ViewModel senkst, verschiebst du ihn aus der Komposition

<ph type="x-smartling-placeholder">
</ph> Der Zustand, der zum ViewModel gezogen wurde, wird außerhalb der Komposition gespeichert.
Abbildung 6: Der Zustand, der zum ViewModel hochgezogen wurde, wird außerhalb der Komposition gespeichert.

ViewModels werden nicht als Teil der Komposition gespeichert. Sie werden vom Framework und sind einem ViewModelStoreOwner zugeordnet, das ein Aktivität, Fragment, Navigationsdiagramm oder Ziel eines Navigationsdiagramms. Für Weitere Informationen zu ViewModel-Bereichen finden Sie in der Dokumentation.

Die ViewModel ist dann die „Source of Truth“ und der niedrigste gemeinsame Ancestor für UI-Status

Status der Benutzeroberfläche des Bildschirms

Wie in den obigen Definitionen beschrieben, wird der Bildschirm-UI-Status durch die Anwendung Regeln. Da der Inhaber der Bildschirmebene dafür verantwortlich ist, bedeutet, dass der Bildschirm-UI-Status typischerweise auf Bildschirmebene gezogen wird. Inhaber, in diesem Fall ein ViewModel.

Betrachte die ConversationViewModel einer Chat-App und wie sie den Bildschirm freigibt UI-Status und Ereignisse zum Ä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 Elemente verarbeiten den Bildschirm-UI-Status, der in ViewModel hochgezogen ist. Sie sollten fügen Sie die Instanz ViewModel in die Zusammensetzungen auf Bildschirmebene ein, Zugriff auf Geschäftslogik.

Hier sehen Sie ein Beispiel für eine ViewModel, die in einer zusammensetzbaren Funktion auf Bildschirmebene verwendet wird. Hier verarbeitet die zusammensetzbare Funktion ConversationScreen() den Status der Bildschirm-UI. in ViewModel:

@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-Drilldown

Bei „Property-Drilldown“ werden Daten durch mehrere verschachtelte untergeordnete Elemente weitergegeben. an die Stelle, an der sie gelesen werden.

Ein typisches Beispiel für die Aufschlüsselung von Eigenschaften in Compose ist, wenn Sie fügen Sie den Inhaber der Bildschirmebene auf der obersten Ebene ein, für zusammensetzbare Funktionen für untergeordnete Elemente. Dies könnte zusätzlich zu einer Überlastung zusammensetzbaren Funktionssignaturen.

Auch wenn Ereignisse als einzelne Lambda-Parameter verfügbar gemacht werden, der Funktionssignatur, maximiert sie die Sichtbarkeit dessen, was die zusammensetzbare Funktion Verantwortlichkeiten haben. Sie sehen auf einen Blick, was passiert.

Die Property-Aufschlüsselung wird gegenüber dem Erstellen von Wrapper-Klassen zum Kapseln vorgezogen. Status und Ereignisse an einem Ort, da dies die Sichtbarkeit der zusammensetzbaren Verantwortlichkeiten. Wenn Sie keine Wrapper-Klassen haben, wahrscheinlich nur die Parameter übergeben, die sie benötigen. Dies ist am besten Übung.

Dieselbe Best Practice gilt, wenn es sich bei diesen Ereignissen um Navigationsereignisse handelt. Sie können Weitere Informationen dazu findest du in der Navigationsdokumentation.

Wenn Sie ein Leistungsproblem festgestellt haben, können Sie das Lesen auch auf später verschieben. des Bundesstaates. Weitere Informationen finden Sie in der Dokumentation zur Leistung.

Status des UI-Elements

Sie können den Status des UI-Elements auf den Inhaber der Bildschirmebene heben, wenn die sie lesen oder schreiben muss.

Am Beispiel einer Chat-App zeigt die App Nutzervorschläge in einer Gruppenchat, wenn der Nutzer @ und einen Hinweis eingibt. Diese Vorschläge stammen aus dem und die Logik zum Berechnen einer Liste von Nutzervorschlägen berücksichtigt, Geschäftslogik. Die Funktion sieht so aus:

<ph type="x-smartling-placeholder">
</ph> Funktion, mit der in einem Gruppenchat Vorschläge für Nutzer angezeigt werden, wenn sie „@“ und einen Hinweis eingeben
Abbildung 7: Funktion, mit der in einem Gruppenchat Nutzervorschläge angezeigt werden, wenn der Nutzer @ eingibt und einen Hinweis eingibt

Der ViewModel, der 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 das Nutzer eine neue Eingabe eingibt, ruft die App die Geschäftslogik auf, um suggestions zu generieren.

suggestions ist der Status der Bildschirm-UI und wird von der Editor-Benutzeroberfläche übernommen, indem folgende Daten erfasst werden: aus dem StateFlow.

Warnung

Für bestimmte Status von UI-Elementen in „Compose“ ist es möglicherweise erforderlich, dass das Heben auf ViewModel besondere Überlegungen. Einige Statusinhaber von UI-Elementen vom Typ „Compose“ explodieren, um den Status zu ändern. Es kann sich dabei um Sperrfunktionen handeln, Animationen auslösen. Diese Aussetzerfunktionen können Ausnahmen auslösen, wenn Sie aus einem CoroutineScope, das nicht dem Komposition

Angenommen, der Inhalt der App-Leiste ist dynamisch und Sie müssen ihn abrufen und aktualisieren nachdem sie geschlossen wurde. Heben Sie den Zustand der Leiste an, Den ViewModel, damit Sie sowohl die UI als auch die Geschäftslogik für dieses Element aufrufen können vom Inhaber des Bundesstaats.

Wenn Sie jedoch die Methode close() von DrawerState mit der Methode viewModelScope aus der Compose-UI führt zu einer Laufzeitausnahme des Typs IllegalStateException mit der Nachricht "a MonotonicFrameClock ist hier nicht verfügbar CoroutineContext”.

Verwende eine CoroutineScope, die sich auf die Komposition bezieht, um dieses Problem zu beheben. Es bietet eine MonotonicFrameClock in der CoroutineContext, die für den aussetzen.

Um diesen Absturz zu beheben, ändern Sie den CoroutineContext der Koroutine in der ViewModel auf einen Wert für die Komposition. 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 Status und Jetpack Compose finden Sie hier zusätzliche Ressourcen.

Produktproben

Codelabs

Videos