Tools und Leistung

In „Verfassen“ über dich hinauswachsen

Lautsprecher

Info

Über den Vortrag

Mit Jetpack Compose lassen sich hochwertige Benutzeroberflächen ganz einfach entwerfen. Es kann jedoch schwierig sein, Probleme zu finden, die sich auf die UI-Leistung auswirken. Solche Probleme können zu Problemen führen, die Ihre Nutzer bemerken, z. B. Startlatenz und ausgelassene Frames, die als Scroll-Jank oder abgehackte Animationen angezeigt werden.

In diesem Vortrag stellen wir Tools vor, mit denen ihr herausfinden könnt, was die Leistung eurer Benutzeroberfläche beeinträchtigt. Dabei konzentrieren wir uns auf Perfetto und das Tracing von (Re-)Kompositionen. Anschließend sehen wir uns bestimmte Anti-Patterns an, die Sie möglicherweise verwenden und die Sie unbemerkt Geld kosten. Außerdem erfahren Sie, wie Sie sie in Ihrem Code finden und durch welche positiven Muster Sie sie ersetzen können. Am Ende dieses Vortrags werden Sie wissen, wie Sie in Compose mit einer leistungsorientierten Denkweise arbeiten und wie Sie Leistungsoptimierungen in Ihrer App profilieren, analysieren und überprüfen können.

Zusammenfassung

  • Perfetto-Traces zeichnen die Aktivität auf dem Gerät auf und zeigen Compose-Slices wie Komposition und Layout im Hauptthread an. Damit können Sie Frame-Zeitachsen prüfen und die Dauer von Phasen verfolgen.
  • Jeder Vorgang innerhalb von Composables ist mit Kosten verbunden, einschließlich des Startens und Abbrechens von Koroutinen. Prüfen Sie APIs mit Nebenwirkungen und erwägen Sie, Effekte mit identischen Schlüsseln zusammenzuführen, um den Aufwand für die Zusammensetzung zu reduzieren.
  • Beim Kompositions-Tracing werden zusammensetzbare Funktionen annotiert. Beim Neukompositions-Tracing (ab Version 1.13) werden Lese- und Schreibvorgänge von Zuständen erfasst, die eine Neukomposition verursachen. Sie helfen bei der Diagnose unerwarteter Arbeit direkt in Perfetto-Traces.
  • ComputedState (Compose 1.13+) vermeidet unerwünschte Invalidierungen und ist bei Read-after-Write-Invalidierungen in der Regel schneller als DerivedState. DerivedState ist weiterhin nützlich, wenn aufwendige Berechnungen oder Zuweisungen vom Caching profitieren.
  • Das Überspringen von Composables verursacht Wartungskosten und Mehraufwand für den Parametervergleich. Verlassen Sie sich auf starkes Überspringen und die Gleichheit der Objektidentität oder vermeiden Sie Invalidierungen vollständig, anstatt Stabilität zu erzwingen.

Fähigkeiten

Ressourcen

Gemini Notebook

Themen

  • jetpack-compose
  • android-performance
  • perfetto
  • Benchmarking
  • Koroutinen
  • composition-tracing
  • android-cli

Transkript

Einführung

  • Hallo.

  • Hallo zusammen.

  • Ich hoffe, Sie hatten eine schöne Pause.

  • Ich hoffe, ihr habt genug Kaffee getrunken, eure Haut gepflegt und so weiter.

  • Heute gehen wir etwas mehr ins Detail.

  • Ja, genau.

  • Heute gehen wir etwas mehr ins Detail.

  • Mein Name ist Andrei Shikov und ich teile mir die Bühne heute mit Andrew Bailey.

  • Wir arbeiten beide im Compose Compiler Runtime-Team, aber auch an vielen anderen Dingen, einschließlich der Leistung.

  • Das Thema des heutigen Vortrags ist die Beantwortung Ihrer Fragen zur Leistung, da Jetpack Compose als Standard-UI-Toolkit für die Android-Entwicklung etabliert ist.

  • Viele fragen sich jetzt, wie sie dafür sorgen können, dass ihre Compose-UI so leistungsstark wie möglich ist.

  • Heute sehen wir uns das Thema also genauer an, wie ich bereits gesagt habe.

  • Außerdem stellen wir Ihnen einige Tools vor, mit denen Sie Ihre Leistung in Compose verbessern können.

  • Zuerst möchte ich darüber sprechen, warum Leistung für uns wichtig ist.

  • Wenn Sie an Qualität und die Entwicklung hochwertiger Apps denken, denken Sie wahrscheinlich an Animationen und einen großen Funktionsumfang. Die Leistung wird dabei manchmal vernachlässigt, aber die Wahrnehmung der Nutzer hängt wirklich von der Leistung Ihrer App ab.

  • Wenn Animationen also ruckeln, wird das sofort wahrgenommen.

  • Alle Messwerte und Zahlen, die wir heute erwähnen, sind also eigentlich Proxy-Messwerte, die wir ausgewählt haben, um darzustellen, wie Nutzer die App, die Animationen und alles, was auf dem Bildschirm passiert, wahrnehmen.

  • Grundsätzlich gilt: Wenn Sie Ihre App optimieren, optimieren Sie sie für eine bessere Nutzererfahrung.

  • Das ist also nur eine Einstellung, die Sie haben sollten, wenn wir uns heute mit vielen Themen befassen.

  • Wenn Sie sich die Leistung Ihrer App ansehen, sollten Sie drei Dinge tun.

  • Wenn Sie die Leistung messen, tun Sie das im Release-Modus. Es gibt also nicht viele Informationen, die sich debuggen lassen, und all das verlangsamt unsere App.

  • Zweitens aktivieren Sie R8 oder andere Minifier.

  • Erst dieses Jahr haben wir eine Optimierung eingeführt, die die Leistung von Koroutinen verdoppelt. Außerdem haben wir viele weitere Verbesserungen vorgenommen.

  • Bei der Optimierung von Compose verlassen wir uns stark auf R8.

  • Und schließlich sollten Sie, insbesondere für die Startmesswerte, ein Baseline-Profil verwenden.

  • Die Klassen, die Sie beim Start laden, werden also vorkompiliert und vorab geladen, wenn die Anwendung gestartet wird.

  • Wie Sie sehen, ist das ein sechsfacher Unterschied im Vergleich zum üblichen debugfähigen Build der App.

  • Und wenn Sie all diese Dinge messen, werden sie sich mit den anderen Erfolgen in Ihrem Code summieren.

  • Wenn Sie all das getan haben und die App immer noch ein wenig verzögert reagiert oder es andere Leistungsprobleme gibt, sollten Sie sich diese Leistungsschleife ansehen.

  • Sie möchten sich einige Messwerte für bestimmte Szenarien ansehen.

  • Wenn Ihre App beispielsweise zu lange zum Laden braucht, können Sie sie zurücksetzen, wie gewohnt über den Launcher starten und prüfen, wie lange es dauert, bis der erste Datenbildschirm angezeigt wird.

  • Oder vielleicht ruckelt die App ein wenig, also scrollen Sie und stellen sicher, dass dieses Scrollen wiederholbar und stabil ist.

  • Die stabile Umgebung auf dieser Folie ist also sehr wichtig, denn wenn Sie später Optimierungen vornehmen, möchten Sie sicher sein, dass sich Ihre Messwerte verbessern und dass diese Optimierungen tatsächlich wie beabsichtigt funktionieren.

  • Benchmarks sind dafür sehr gut geeignet, weil sie bereits viele Daten erfassen und viele Iterationen durchführen, um sicherzustellen, dass es nicht zu viele Störfaktoren auf Ihrem Gerät gibt. Außerdem führen sie viele Geräteeinstellungen durch, die Ihnen helfen.

  • Benchmarks sind standardmäßig auch wiederholbare Szenarien, und das ist es, was wir hier anstreben.

  • Sie können aber auch mehrere Skripts verwenden, wenn Sie möchten.

  • Und schließlich erstellen Sie ein Profil, verbessern etwas in Ihrer App, sehen sich die Daten an und wiederholen den Vorgang, vielleicht mit anderen Szenariomesswerten, bei denen Ihre App Probleme hat.

Profilerstellung mit Perfetto-Traces

  • Das erste Tool, das wir heute vorstellen und mit dem Sie sich vertraut machen sollten, sind die Perfetto-Traces.

  • Perfetto-Traces zeichnen alles auf, was auf dem Gerät passiert, während Sie den Trace aufzeichnen, z. B. während der Ausführung eines Makrobenchmarks oder eines Scripts, das Sie aufzeichnen.

  • Und es ist eine große Hilfe für alle Entwickler, die an der Android-Leistung arbeiten.

  • Ich verbringe die meiste Zeit meiner Arbeit damit, mir die Perfetto-Traces anzusehen.

  • Sie sehen in etwa so aus.

  • Das sind eine Menge Daten.

  • Wie gesagt, sie erfassen alles, was auf einem Gerät passiert.

  • Es kann also sein, dass Sie Ihre App nicht sofort finden. Ich empfehle daher, die praktische Suchleiste zu verwenden.

  • Suchen Sie nach Compose, nach Ihrem App-Paket und Ähnlichem.

  • Konzentriere dich einfach auf vertraute Dinge.

  • Wenn Sie sich orientiert und den Prozess Ihrer App gefunden haben, sehen Sie die Frame-Time-Linien und wie lange viele Dinge in Ihrer App dauern.

  • Wenn wir einen davon isolieren, sieht das in etwa so aus.

  • Der erste ist die Arbeit im Hauptthread.

  • Hier erledigt Compose die meiste Arbeit.

  • Dann gibt es noch RenderThread.

  • Hier verarbeiten wir den Canvas-Befehl und codieren ihn für die GPU.

  • Dann erledigt die GPU ihre Arbeit und das System setzt das Ergebnis zusammen.

  • Wir sehen uns heute nur den Hauptthread an.

  • Denn hier erledigt Compose seine Arbeit.

  • Wir sprechen über Compose, also ist es sinnvoll, uns nur darauf zu konzentrieren.

  • Wenn wir diesen einen Frame im Hauptthread isolieren, sehen wir jetzt Compose-Slices.

  • Ich habe einige davon für dich ausgewählt.

  • Es geht um Komposition, Messen, Layout, Zeichnen.

  • Wenn Sie schon einmal von Compose-Phasen gehört haben, haben Sie sich wahrscheinlich schon einmal die Leistung angesehen.

  • Sie haben also einzelne Segmente in einem Perfetto-Trace und es ist sehr einfach, herauszufinden, wie lange jede Phase dauert und welche Phasen in jedem einzelnen Frame ausgeführt werden.

  • Wir haben ausführlich darüber gesprochen, wie sich diese Arbeit optimieren lässt. Sie sollten darauf achten, die richtigen Aufgaben in der richtigen Phase zu erledigen.

  • Wenn Sie beispielsweise eine Animation haben, die die Farbe eines Felds aktualisiert, möchten Sie nur die Zeichenphase ungültig machen, nicht aber die Komposition, das Layout und ähnliche Dinge.

  • Das ist die Grundlage für die Leistung. Heute gehen wir etwas mehr ins Detail und konzentrieren uns insbesondere auf die Zusammensetzung.

  • Andrew wird uns jetzt mehr dazu erzählen.

  • Vielen Dank, Andrei.

  • Wie Andrei bereits erwähnt hat, haben wir im Zusammenhang mit der Leistung viel über Optimierungen gesprochen, z. B. über das Vermeiden von Rückwärtslesevorgängen, das Verschieben von Statuslesevorgängen in die richtige Phase des Lebenszyklus der Komposition und die Optimierung, um sicherzustellen, dass Ihre Composables wie erwartet ungültig werden, damit Sie nicht bei jedem einzelnen Frame zu viel Arbeit haben.

  • Das ist ein wirklich guter Ausgangspunkt, aber wir haben bereits viele Informationen dazu und möchten das Thema etwas weiterentwickeln.

  • Heute sehen wir uns an, was nach diesen Optimierungen in der Komposition übrig bleibt, und was die zusammensetzbaren Funktionen selbst tun.

  • Die traurige Realität bei allem, was sich in Ihrem Compose-Code befindet, und eigentlich bei allem, was Sie mit Ihrer App in Bezug auf die Leistung ausliefern, ist, dass alles mit Kosten verbunden ist.

  • Kein Code, den Sie in Ihrer App bereitstellen, ist kostenlos. Selbst so triviale Dinge wie das Lesen des Status und das Erstellen von Dingen wie Nebenwirkungen innerhalb Ihrer Komposition sind mit spürbaren Kosten verbunden. Es ist wichtig, sich dessen bewusst zu sein, insbesondere wenn Sie beginnen, die tatsächliche Funktionsweise Ihrer Kompositionen zu analysieren und die Leistung Ihrer App zu optimieren.

  • Um zu zeigen, wie das auf mikroskopischer Ebene aussieht, sind hier einige der kleinsten Vorgänge, die Sie überall in Ihrem Code und insbesondere in Ihren Composables ausführen können. Bei dieser Auflösung sprechen wir von der Nanosekundenskala, was die ungefähren Kosten dieser Dinge angeht.

  • Wenn Sie Ihre Leistung verbessern möchten, haben Sie zwei Möglichkeiten.

  • Sie können entweder eine sehr große Anzahl kostengünstiger Vorgänge oder eine kleinere Anzahl teurerer Vorgänge entfernen.

  • In diesem Diagramm sind Koroutinen im Vergleich zu anderen Vorgängen eher auf der teureren Seite. Das ist ein gutes Beispiel dafür, wo man mit der Suche beginnen sollte, denn wenn wir einige dieser Instanzen entfernen, können wir schneller greifbare Vorteile in unserer Anwendung sehen.

  • Eine weitere Sache, die Sie bei Koroutinen beachten sollten, ist, dass nicht nur das Starten einer Koroutine mit Kosten verbunden ist, sondern auch das Abbrechen einer Koroutine.

  • Die Kosten für die Stornierung sind besonders bemerkenswert, da sie hauptsächlich beim Verwerfen von Inhalten und möglicherweise beim Wechsel zu anderen Bereichen in Ihrer Anwendung anfallen.

  • Das Verlassen der Seite ist in vielen Anwendungen ein extrem teurer Fall, bei dem Sie die Frame Time optimal nutzen und sicherstellen müssen, dass Sie die vorhandenen Ressourcen optimal nutzen.

  • Daher ist es wichtig, beide Kosten im Blick zu behalten.

  • All diese Dinge, egal, was wir uns ansehen, summieren sich auf dieser Skala von etwa 16 Millionen Nanosekunden, die uns zur Verfügung stehen.

  • Wir haben hier also ein gewisses Budget, das wir reduzieren müssen, und Koroutinen sind ein gutes Beispiel für den Anfang.

Umgang mit Nebenwirkungen

  • In Compose werden Koroutinen am häufigsten mit den Effects APIs verwendet.

  • Es gibt eine Reihe von APIs, die Sie mit SideEffects verwenden können, von hoher Funktionalität bis hin zu niedriger Latenz.

  • Am funktionalsten Ende dieses Spektrums befindet sich LaunchedEffect. Es enthält alles für asynchrone Arbeit, insbesondere Animationen und alles, wofür Sie einen Coroutine-Bereich benötigen.

  • Wenn Sie das jedoch nicht benötigen und Ihre Aufgabe mit einer günstigeren API erledigen können, sollten Sie diese stattdessen verwenden, sofern sie den Anforderungen Ihres Anwendungsfalls entspricht.

  • Um zu zeigen, wie das in der Praxis aussieht, habe ich hier ein Media Player-Composable, mit dem ich alle verschiedenen SideEffects und die Situationen, in denen sie auftreten können, demonstrieren kann.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • Wir können mit einem LaunchedEffect beginnen.

  • In diesem Beispiel wird es verwendet, um Daten asynchron im Mediaplayer zu laden.

  • Wenn Sie einen Koroutinenbereich benötigen oder Koroutinen im Allgemeinen oder für einen aussetzenden Aufruf benötigen, verwenden Sie LaunchedEffect.

  • Auch wenn sie immer noch zu den teureren Optionen gehören und mit den Koroutinen Kosten verbunden sind, sollten Sie die richtige API für die Aufgabe verwenden, solange Sie sie nutzen und rechtfertigen können.

  • Wenn Sie jedoch einen LaunchedEffect verwenden, aber die Koroutine nicht nutzen, sollten Sie andere APIs in Betracht ziehen, die dasselbe tun, aber weniger kosten.

  • Wenn wir uns das Spektrum weiter ansehen, finden wir andere Beispiele für Nebenwirkungen wie DisposableEffect.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val activity = LocalActivity.current
    
        // Setup and teardown
        DisposableEffect(Unit) {
            activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
            onDispose {
                activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
            }
        }
    }
  • DisposableEffect eignet sich hervorragend, wenn Sie einen Einrichtungs- und Abbauprozess mit Ihrem Effekt verknüpft haben.

  • Hier ist ein Beispiel für denselben Mediaplayer, bei dem die Ausrichtung der Aktivität auf Querformat gesperrt wird, wenn das Composable vorhanden ist, und die Ausrichtungssperre aufgehoben wird, wenn es später nicht mehr vorhanden ist.

  • Wenn Sie den einmaligen Effekt von LaunchedEffect und DisposableEffect nutzen möchten, aber keine Koroutinen benötigen und keinen Dispose-Effekt haben, können Sie auch die neue SideEffect-API mit Schlüsseln verwenden, die wir in Compose 1.12 eingeführt haben.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • Dies ist eine gute Alternative zu den anderen beiden APIs, wenn Sie diese entweder LaunchedEffect aus Bequemlichkeit oder DisposableEffect aus Notwendigkeit für einen einmaligen Effekt verwendet haben. Wenn Sie zu den SideEffect-APIs mit Schlüsseln wechseln, können Sie dasselbe mit weniger Aufwand und verbesserter Leistung erreichen, insbesondere wenn Sie mehrmals über die gesamte App hinweg migrieren.

  • Die günstigste API ist die Standard-SideEffect-API.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • Dies ist am besten geeignet, wenn Sie den externen Status der Welt mit dem synchronisieren möchten, was in der Komposition passiert.

  • Hier ist ein weiteres Beispiel für den Mediaplayer, bei dem der Wiedergabestatus mit einem Parameter synchronisiert wird, der von der Komposition empfangen wird.

  • Wenn Sie sich entscheiden, Ihre Nutzung von Nebeneffekten zu prüfen und zu einer kostengünstigeren Variante zu migrieren, sollten Sie beachten, dass es neben den anderen Verhaltensweisen und Anwendungsfällen, über die wir gesprochen haben, auch einen Unterschied bei den Garantien für die Reihenfolge bei Nebeneffekten gibt.

  • Zuerst werden alle DisposableEffects ausgeführt, gefolgt von den regulären SideEffects, mit oder ohne Schlüssel. Nachdem der Frame abgeschlossen ist, werden alle LaunchedEffects ausgeführt, da ein Dispatcher beteiligt ist.

  • Wenn Sie für Ihre gestarteten Effekte auf dieses Timing angewiesen sind, sollten Sie das bei einer Migration berücksichtigen.

  • Und testen Sie bei jeder Migration Ihren Code, um sicherzustellen, dass Sie nicht von diesen Zeitgarantien abhängig sind und dass Ihr Code danach noch funktioniert.

  • Wenn Sie die Komplexität der Nebenwirkung selbst nicht reduzieren können, können Sie die Anzahl der Nebenwirkungen reduzieren, indem Sie sie zusammenführen.

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) { viewModel.loadCart() }
        LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
        LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
    
        CartContent(...)
    }
  • Das ist besonders wichtig, wenn Sie mehrere Effekte haben, die denselben Schlüssel verwenden.

  • Sie sind gute Kandidaten für eine Zusammenführung, da sie gleichzeitig ausgeführt werden können.

  • In diesem Beispiel habe ich mehrere gestartete Effekte, die jeweils einen asynchronen Aufruf an ein Ansichtsmodell senden.

  • Wenn ich sie durch ein einzelnes LaunchedEffect und dann andere Startaufrufe darin ersetze, habe ich zwar immer noch die gleiche Anzahl von Coroutinen, die hier ausgeführt werden, aber der Overhead ist geringer, weil die Arbeit nicht mehr in der Komposition, sondern in Coroutinen erledigt wird.

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) {
            launch { viewModel.loadCart() }
            launch { viewModel.loadPaymentMethods() }
            launch { viewModel.loadDeliveryAddresses() }
        }
    
        CartContent(...)
    }
  • In solchen Situationen ist es daher sinnvoll, die Migration an bestimmten Stellen in Betracht zu ziehen, um die Anzahl der Effekte nach Möglichkeit zu reduzieren.

  • Effekte und Koroutinen sind nur ein Beispiel für einen Bereich, den Sie in Ihrer Anwendung prüfen können. Jede komponierbare Funktion und jede Anwendung ist jedoch anders und bietet unterschiedliche Möglichkeiten.

  • Um zu verstehen, was für Ihre Anwendung relevant ist, müssen wir über das nächste Tool sprechen, das Andrei vorstellen wird: das Composition Tracing.

Tracing von Komposition und Neukomposition

  • Okay.

  • Wenn Sie sich diese originalgetreue Nachbildung des Perfetto-Frames ansehen, den wir zuvor auf einer anderen Folie gesehen haben, sehen Sie, dass wir Timing-Informationen mit verschiedenen Phasen erhalten, aber nicht wirklich viele Details darüber, was passiert.

  • Was?

  • Ja, was in der Komposition passiert, in der zusammensetzbare Funktionen ausgeführt werden.

  • Und das Composition Tracing hilft uns dabei.

  • Wir führen also einige Markierungen im Compose-Compiler ein, die im Grunde jede Funktion annotieren und im Perfetto-Trace sichtbar machen, damit Sie prüfen können, wie lange jede Funktion dauert.

  • Damit können Sie herausfinden, welche Funktionen genau neu zusammengesetzt werden.

  • Das ist jedoch mit einem gewissen Aufwand verbunden. Wenn Sie also möglichst genaue Leistungsdaten erfassen möchten, sollten Sie das reguläre Perfetto verwenden oder einfach ohne Perfetto-Erfassungen arbeiten und andere Messwerte verwenden.

  • Das Composition Tracing ist jedoch sehr nützlich, wenn Sie herausfinden möchten, wie lange die einzelnen Composables dauern.

  • Manchmal möchten Sie auch herausfinden, warum Elemente neu zusammengesetzt werden. Das versuchen wir in der nächsten Version des Compose Tracing-Laufzeit-Tracing, also 1.13, zu lösen.

  • Wir führen jetzt das Tracing von Neukompositionen ein, bei dem jeder State Read und State Write, der zu einer Neukomposition geführt hat, mit Anmerkungen versehen wird.

  • In Ihrem Perfetto-Trace erhalten Sie dann eine Ausgabe wie diese, in der Sie die einzelnen Zustandsschreibvorgänge und die Aufrufstapel sehen, in denen der Schreib- und Lesevorgang stattfindet. So können Sie sie den tatsächlichen Zeilen in Ihrem Code zuordnen, in denen auf diese Zustände zugegriffen oder sie geändert wurden.

  • Wenn Sie mehr erfahren möchten, rufen Sie den Link hier auf.

  • Dort finden Sie die gesamte Dokumentation zum Composition- und Recomposition-Tracing.

  • Jetzt übergebe ich an Andrew, der Ihnen erklärt, wie Sie dies effizienter nutzen können, um Zustandsschreibvorgänge zu vermeiden.

Statusoptimierung und -entwertungen

  • Vielen Dank, Andrei.

  • Anhand der Ergebnisse dieser Untersuchung können Sie feststellen, ob es in Ihrer Komposition ungültige Elemente gibt, die für Ihre Anwendung nicht nützlich sind und von vornherein hätten vermieden werden können.

  • Das häufigste Beispiel, das ich hier habe, ist eine Schaltfläche zum Scrollen nach oben, deren Sichtbarkeit von der Sichtbarkeit oder dem Scrollfortschritt abhängt.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton = scrollState.value > 0
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • Da ich hier den Wert eines Scrollstatus innerhalb der Komposition lese, ist dies ein Status, der sich sehr häufig ändert, möglicherweise einmal pro Frame. Dies führt zu vielen Neuzusammensetzungen, die vermieden werden könnten, da sich die Bedingung, die uns hier interessiert, nicht so oft ändert, obwohl sich der Scrollstatus selbst ändert.

  • In diesem Fall empfehlen wir, einen berechneten Zustand zu verwenden, um ungültige Zustandsänderungen zu vermeiden, die für Ihre Anwendung nicht relevant sind.

  • In diesem Beispiel habe ich die Berechnung selbst in einen berechneten Zustand eingeschlossen. Wenn wir diesen Zustand lesen, wird die Validierung nicht mehr anhand der Scrollposition selbst aufgehoben, sondern die Validierung dieser komponierbaren Funktion wird bedingt anhand der gesamten Bedingung aufgehoben und vom anderen Zustand selbst abgeleitet.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • Wenn Sie dieses Muster schon einmal gesehen haben, wurde es wahrscheinlich mit einem abgeleiteten Zustand verwendet.

  • „ComputedState“ ist neu in Compose 1.13. „ComputedState“ und „DerivedState“ haben zwar beide ein ähnliches Ziel, nämlich die bedingte Invalidierung, wenn sich ihre Berechnung ändert, aber sie unterscheiden sich geringfügig in der Art und Weise, wie sie das tun.

  • „DerivedState“, das wir seit Beginn von Compose anbieten, speichert den berechneten Wert im Cache und verwendet ihn wieder, bis sich die Abhängigkeiten ändern.

  • Mit „ComputedState“ wird der Wert jedes Mal berechnet, wenn Sie ihn lesen.

  • Das Caching klingt zwar nützlich, ist aber in vielen Situationen teurer, als nur das Ergebnis zu lesen.

  • Um zu verstehen, wie das funktioniert, sehen wir uns ein Beispiel dafür an, wie diese Zustände in Ihrer Anwendung verwendet werden könnten, und gehen darauf ein, was die einzelnen Zustände tatsächlich tun.

  • Hier habe ich denselben Zustand, der in etwa dem entspricht, was wir mit der Schaltfläche „Nach oben scrollen“ gemacht haben. Es gibt eine Bedingung, die auf einem anderen Zustand basiert, und ich habe sie sowohl als DerivedState als auch als ComputedState.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    // First read
    derivedState.value
    computedState.value
    
    // Second read
    derivedState.value
    computedState.value
  • Ich lese den Status auch zweimal und erhalte beide Male denselben Wert, aber die Vorgänge im Hintergrund unterscheiden sich.

  • Um einen Trace dessen, was hier tatsächlich in einem Tool wie Perfetto angezeigt wird, zu vereinfachen, können wir uns ansehen, was tatsächlich passiert, wenn Sie beide Zustände lesen.

  • Hier rufen wir zuerst die computedState.value auf.

  • Die Lambda-Funktion für die Berechnung wird ausgeführt, die dann den Wert des Zustands liest. Außerdem wird eine Benachrichtigung gesendet, dass der Lesevorgang stattgefunden hat.

  • Das gilt für alle Snapshot-Zustände in Compose.

  • Der zweite Lesevorgang verhält sich genau wie der erste und hat durchgehend dieselben Leistungsmerkmale.

  • Im Vergleich dazu passiert beim allerersten Lesen des DerivedState etwas Ähnliches: Es muss immer noch das Berechnungs-Lambda aufrufen und die andere Benachrichtigung ausführen, aber es gibt ein wenig Cache-Verwaltungsaufwand, der beim ersten Mal etwas verkürzt wird, da kein Cache zu prüfen ist.

  • Es wird also ein wenig Aufwand für das Caching betrieben und dann wird dasselbe in nur etwas mehr Zeit erreicht.

  • Der zweite Lesevorgang für „DerivedState“ gewinnt jedoch tatsächlich gegen den zweiten Lesevorgang für „ComputedState“, obwohl beide Zustände ziemlich trivial sind.

  • Wie ich auf den vorherigen Folien erwähnt habe, sind die Kosten für einen Snapshot-Lesevorgang in einigen Fällen nicht unerheblich. Das Lesen aus dem Cache ist schneller als der Lesevorgang selbst. Deshalb sehen wir diesen sehr geringen Vorteil für den zweiten Lesevorgang für den DerivedState.

  • Das eigentlich interessante Szenario ist jedoch, was passiert, wenn der Status ungültig gemacht werden muss.

  • Das ist das sogenannte „Read-after-Write“-Szenario, bei dem Sie einen der in der Berechnung verwendeten Zustände aktualisieren und die Berechnung noch einmal ausgeführt werden muss.

  • Im Code sieht das so aus: Wir ändern „a“, erhöhen es vielleicht, lesen den Wert dann noch einmal oder führen vielleicht eine Neuzusammensetzung durch.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    ...
    
    // Read-after-write
    a++
    derivedState.value
    computedState.value
  • Im eigentlichen Diagramm sieht das für „ComputedState“ genauso aus.

  • Es gibt keinen Cache, der verwaltet werden muss. Das sieht genau gleich aus, egal ob es sich um den ersten, zweiten, dritten oder einen anderen Lesevorgang handelt.

  • DerivedState hat jedoch einiges zu tun.

  • Es wird ein sehr aufwendiger Pfad durchlaufen, um festzustellen, dass der Cache ungültig ist, bevor er überhaupt aktualisiert wird und die Berechnung selbst aufruft. Anschließend muss die Abhängigkeit verfolgt und ein Großteil der Beobachtung verwaltet werden, um den Cache-Wert zurückzuschreiben und zu verstehen, dass sich alle diese Werte tatsächlich geändert haben, und die Verwaltung selbst zu übernehmen.

  • All das summiert sich und macht die Kosten für die Invalidierung eines DerivedState im Vergleich zu einem ComputedState sehr hoch.

  • Um diese Behauptungen mit Zahlen zu untermauern: ComputedState ist beim ersten Lesen in der Regel etwa 200 ns schneller.

  • ComputedState verliert unter bestimmten Benchmarking-Bedingungen auf einem Pixel 6 etwa 350 ns.

  • Beim zweiten Lesen verliert „ComputedState“ also im Vergleich zu „DerivedState“ ein wenig.

  • In dem Fall, der uns eigentlich interessiert, also wenn wir die Daten ungültig machen und wahrscheinlich auch Arbeit am Frame haben, ist ComputedState fast und sogar mehr als doppelt so schnell wie DerivedState.

  • Je nachdem, wofür Sie Ihre Zustände verwenden, kann „ComputedState“ eine sehr gute erste Wahl sein.

  • Im Allgemeinen muss bei der Verwendung von DerivedState und ComputedState immer noch eine Entscheidung getroffen werden.

  • Wir stellen „ComputedState“ nicht ein, da es für beide noch Anwendungsfälle gibt.

  • Wenn Sie sich nicht sicher sind, welche Option Sie auswählen sollen, ist der berechnete Status im Allgemeinen eine gute Standardeinstellung.

  • Der abgeleitete Zustand ist am nützlichsten, wenn Sie das Caching tatsächlich nutzen, d. h. wenn Sie etwas Komplexes tun, z. B. einen komplizierten Algorithmus, eine große Zuweisung, viele Zustandslesevorgänge, die tatsächlich vom Caching profitieren, oder etwas Komplexes oder etwas, das von Dingen wie Objektgleichheit und Objektidentitätsgleichheit abhängt, wobei Sie den Unterschied ausgleichen und die Vorteile des Cachings nutzen, die der abgeleitete Zustand bietet.

Überspringen und Stabilität in Compose

  • Ein weiteres wichtiges Thema, das ich ansprechen möchte, ist, dass sowohl der abgeleitete als auch der berechnete Zustand versuchen, die Invalidierung der komponierbaren Funktion selbst zu vermeiden.

  • Das ist etwas anderes als das Überspringen einer zusammensetzbaren Funktion, worüber wir normalerweise sprechen, wenn wir über das bedingte Invalidieren oder das Nichtausführen von Arbeit innerhalb einer zusammensetzbaren Funktion nachdenken.

  • Ich möchte hier klarstellen, dass Stabilität und Überspringen in Compose zwar viel diskutiert werden, aber nicht das Nonplusultra sind und nicht immer das Richtige, um dafür zu optimieren.

  • Wie bereits erwähnt, ist alles in der Komposition mit Kosten verbunden, auch das Überspringen.

    // What you write
    @Composable fun ContactListItem(
        contact: Contact
    ) { ... }
    
    // What you get
    @Composable fun ContactListItem(
        contact: Contact,
        $composer: Composer?
        $changed: Int
    ) {
        val $dirty = $changed or
            $composer.changed(contact)
    
        if ($dirty) { /* Your function body */ }
        else $composer.skipToGroupEnd()
    }
  • Das Überspringen ist nicht unbedingt trivial.

  • Es ist viel Aufwand nötig, um sie überhaupt zu aktivieren.

  • Wir müssen beispielsweise alle Ihre Parameter speichern und bei der ersten Komposition Platz dafür finden und dann diesen Eintrag aktualisieren, wenn Sie eine Neukomposition vornehmen.

  • Außerdem müssen wir Ihre Werte mit den vorherigen Werten vergleichen, um festzustellen, ob Ihre zusammensetzbare Funktion übersprungen werden soll oder nicht.

  • Danach wird alles nur noch durch eine if-Prüfung gesteuert, die den Code überspringt oder nicht.

  • Das kann je nach Situation teuer werden.

  • Der Compiler wird im Hintergrund optimiert. Wenn Sie denselben Parameter übergeben, können wir Gleichheitsprüfungen wiederverwenden. Das ist aber nicht die einzige Möglichkeit und es ist ziemlich einfach, diese Optimierung zu umgehen.

  • Ein klassisches Beispiel dafür, dass das Überspringen nicht immer funktioniert, ist eine scrollbare Liste, die ein sehr großes Objekt enthält, das alle auf dem Bildschirm darzustellenden Daten enthält.

    data class ContactsModel(
        val header: String,
        val contacts: List<Contact>
    )
    
    data class Contact(...)
    
    @Composable
    fun ContactsScreen(model: ContactsModel) {
        Column {
            TopAppBar(title = { Text(model.header) })
            ContactsList(model.contacts)
        }
    }
    
    @Composable
    fun ContactsList(contacts: List<Contact>) {
        LazyColumn {
            items(contacts) { ContactListItem(it) }
        }
    }
    
    @Composable
    fun ContactListItem(contact: Contact) { ... }
  • Hier enthält das Bildschirmmodell also so etwas wie einen Header-Bildschirm und dann die gesamte Liste der Kontakte, die dann an ein paar Composables übergeben werden, die schließlich eine Lazy List aufrufen und sie dann mit einigen Elementzellen auf dem Bildschirm anzeigen.

  • Die Kompositionshierarchie für diesen Code sieht in etwa so aus: Wir beginnen wieder mit dem Kontaktbildschirm, fächern ihn ein wenig auf und haben dann einige andere Composables, die unsere Elemente aufrufen und darstellen.

  • Wenn wir uns ansehen, wie unsere Daten weitergegeben werden, beginnen wir mit dem Modell oben.

  • Wir zerlegen ihn in einen String und dann in eine Liste. Diese Liste wird dann in ein Element-Lambda für die Lazy List konvertiert. Die Lazy List setzt dann alle Elemente zusammen, die auf dem Bildschirm angezeigt werden.

  • Sehen wir uns an, was passiert, wenn wir den letzten Kontakt in dieser Liste ändern.

  • Wir möchten, dass nur das letzte Element in der Liste ungültig wird, aber das ist nicht das, was hier passiert.

  • Wenn wir uns einen Kompositions-Trace für diese Neuzusammensetzung ansehen, sieht er in etwa so aus: Wir beginnen beim Kontaktbildschirm und arbeiten uns dann bis zu diesem Listenelement vor.

  • Und das ist eine ziemlich teure Methode, um hierher zu kommen.

  • Außerdem gibt es alle diese Abschnitte, in denen im Trace scheinbar nichts passiert.

  • Auch wenn es hier nicht klar ist, entscheiden die Composables hier tatsächlich, ob sie übersprungen werden oder nicht.

  • In diesem Beispiel werden nur zwei Composables übersprungen, da die Lazy List neu zusammengesetzt wird.

  • Wenn Sie ihr ein neues Content-Lambda übergeben, werden alle vorhandenen Elemente ungültig.

  • Die beiden Kontakte, die sich nicht geändert haben, werden also zusätzlich zu dem Kontakt, der sich geändert hat, ungültig gemacht. Diese beiden sind die einzigen Elemente, die hier übersprungen werden.

  • Alles andere führt diesen überspringenden Gleichheitsvergleich nur durch, um festzustellen, dass es tatsächlich ausgeführt werden muss.

  • Wenn Ihre Objekte stabil sind, kann dieser Vergleich tatsächlich ziemlich teuer sein.

  • Wenn Sie uns zwingen, alle Ihre Datenstrukturen zu durchlaufen, kann dies zu erheblichen Kosten führen, nur um festzustellen, ob zwei Objekte gleich sind.

  • Und was noch schlimmer ist: Da wir die Liste destrukturiert haben, müssen wir denselben Vergleich zweimal durchführen.

  • Wenn Sie stattdessen das Standardverhalten des Compilers zulassen und das starke Überspringen verwenden, können Sie weiterhin das Überspringen und Prüfungen zum bedingten Überspringen Ihrer Composables nutzen. Sie gehen jedoch den viel schnelleren Weg, die Gleichheit der Objektidentität anstelle der strukturellen Gleichheit zu verwenden.

  • Das geht viel schneller. Auch wenn wir dieselben Elemente in diesem Frame neu zusammensetzen, wo es eigentlich darauf ankommt, dass wir schnell sind, verschwenden wir nicht zu viel Zeit damit, herauszufinden, dass wir diese Composables nicht überspringen konnten.

  • Aus diesem Grund empfehlen wir auch, Tools wie Benchmarks und Traces zu verwenden, um zu verstehen, was in Ihrer Anwendung passiert, anstatt direkt die Anzahl der Neuzusammensetzungen zu zählen.

  • Sie können ein nützlicher Proxy sein, aber Sie sollten sich bei der Optimierung Ihrer Anwendungen nicht zu sehr darauf konzentrieren oder sie überbewerten.

  • Denken Sie daran, dass Sie nicht versuchen sollten, das Composable zu überspringen, sondern es gar nicht erst ungültig zu machen.

Ausführung mit Methoden-Traces analysieren

  • In diesem Beispiel haben wir gesehen, dass Traces zur Zusammensetzung viele Informationen darüber liefern können, was passiert, aber nicht unbedingt das gesamte Bild zeigen.

  • Andrei wird uns dazu etwas über Methoden-Traces erzählen.

  • Okay.

  • Wie Andrew bereits erwähnt hat, liefert das Composition Tracing nicht immer alle Informationen, da es nur zusammensetzbare Funktionen annotiert. Es kann vorkommen, dass im Composition Trace, der den Perfetto-Trace bereits ergänzt, nur leerer Raum zu sehen ist, aber dennoch nicht alles angezeigt wird, was dort passiert.

  • Manchmal liegt es an Compose, manchmal an Ihrem Code. Es ist wirklich schwierig, das nur anhand des hier gezeigten Composition-Trace herauszufinden.

  • Als Nächstes müssen Sie den Methoden-Trace abrufen.

  • Es gibt ein Element im Profiler in Android Studio, oder Sie können auch ADB-Befehle verwenden, um den Trace zu erfassen. Sie erhalten dann jeden Funktionsaufruf, der in der Java-Welt stattfindet.

  • Es enthält alles, was der Compose-Compiler dort codiert, alles, was die Compose-Laufzeit tut, und alles, was Ihr Code tut. Es ist wirklich nützlich, um zu verfolgen und zu untersuchen, was genau ausgeführt wird und welche Funktionen in diesem Bereich aufgerufen wurden.

  • Eine Sache möchte ich noch erwähnen – sehen Sie sich dieses Element in der Mitte an.

  • Ich möchte darauf hinweisen, dass das Methoden-Tracing einen sehr hohen Overhead hat.

  • Je nach Komplexität des Funktionsbaums ist der Overhead 10- bis 20-mal höher, da jeder Funktionsaufruf ein wenig dazu beiträgt.

  • Zur Veranschaulichung sehen wir uns den Screenshot an.

  • So sieht der Methoden-Trace für den Start der App normalerweise aus. Sie fragen sich vielleicht, wie Sie das interpretieren sollen.

  • Normalerweise beginnen Sie mit dem Kompositions-Trace und sehen sich die Funktionen an, die Sie hier haben.

  • Das ist also wie ein vereinfachter Kompositions-Trace, der „ScreenContent“, einen „Button“ und einige andere Funktionen enthält.

  • Angenommen, wir möchten herausfinden, was in „ScreenContent“ passiert.

  • Perfetto hat eine Suchleiste.

  • Sie laden Ihren Methoden-Trace in den Perfetto-Viewer und suchen nach derselben Funktion wie hier, „ScreenContent“. Sie erhalten dann etwas Ähnliches, nämlich den Methoden-Trace für den Bildschirm.

  • Hier sehen Sie also einige Composer.startGroup- und Gruppenanrufe.

  • Das machen Laufzeiten und Compiler im Hintergrund, damit die Komposition tatsächlich funktioniert. Der interessante Aufruf hier ist jedoch Composer.changed.

  • So prüfen wir, ob sich die Parameter zwischen vorherigen Aufrufen und dem aktuellen Aufruf der zusammensetzbaren Funktion geändert haben.

  • Und die meisten dieser Composer.changed werden von diesem ScreenModel.equals dominiert.

  • Vielleicht ist das ScreenModel eine Art Big-Data-Klasse und wir haben es stabil gemacht, um die Anzahl der Neuzusammensetzungen zu reduzieren. Vielleicht ist es also etwas, das im Compilerbericht aufgetaucht ist.

  • Vielleicht hilft es in diesem Fall, die stabile Annotation zu entfernen, um die Leistung zu verbessern.

  • Danach sollten Sie also suchen. Methoden-Traces für die Zusammensetzung sind das beste Tool, um diese zu finden.

  • Okay.

  • Wir haben uns heute viele Dinge angesehen. Jetzt übergebe ich an Andrew, der die Veranstaltung abschließen wird.

Schnelle Tools für das komponierbare Design und die Leistung

  • Zum Abschluss und als Zusammenfassung von allem, worüber wir heute gesprochen haben, gibt es ein paar Dinge, die Sie beim Entwerfen von Composables und beim Optimieren Ihrer vorhandenen Apps beachten sollten.

  • Denken Sie zuerst daran, dass günstigere Composables und Composables, die Sie nicht entwerten, besser sind als Composables, die Sie überspringen.

  • Denken Sie beim Entwerfen von Composables daran, dass es besser ist, kleinere und leichter zu vergleichende Argumente zu verwenden und möglicherweise Vergleiche mit anderen Funktionsaufrufen wiederzuverwenden, um die Optimierung zu nutzen.

  • Wenn Sie APIs in Ihren Composables verwenden, sollten Sie beachten, dass die Leistung oder die Frame Time ein Produkt der Kosten der APIs selbst und der Häufigkeit ihrer Verwendung ist.

  • Beachten Sie, wie sich diese Kosten summieren, und dass möglicherweise auch Kosten für den Abbau anfallen, die Sie zum ungünstigsten Zeitpunkt für Ihre Anwendung treffen können.

  • Außerdem ist es wichtig, dass Sie sich bei der Optimierung Ihrer App an die Regeln halten.

  • Beginnen Sie mit Benchmarks, um fundierte Entscheidungen zu treffen, erstellen Sie ein Profil Ihrer App, um sicherzustellen, dass Sie die richtigen Bereiche Ihres Codes ansprechen, und prüfen Sie, ob Sie die Änderungen vornehmen und die Ergebnisse sehen, die Sie von den Änderungen erwarten, die Sie aufgrund von Benchmarks und Profiling vornehmen.

  • Denken Sie auch daran, dass es in Ihrer Anwendung zu einem Kompromiss zwischen Wartbarkeit und Leistung kommen kann.

  • Tun Sie also, was für Sie und Ihr Team am besten ist, damit Sie nicht zu viel optimieren und sich bei der Wartbarkeit in eine Sackgasse manövrieren.

  • Und wenn Sie weitere Hilfe benötigen, steht Ihnen ein weiteres Tool zur Verfügung: der KI-Agent, den Sie bereits haben.

  • Wir haben im Rahmen der Android-Befehlszeile und der dort bereitgestellten Skills einen Jetpack Compose-Leistungs-Skill eingeführt, der viele der heute besprochenen Informationen enthält. Er kann Ihre Anwendung prüfen und Vorschläge für diese Art von Optimierungen machen.

  • Sie können es mit der Android-Befehlszeile in Ihrem vorhandenen Agenten installieren.

    android skills add jetpack-compose-performance
  • Weitere Informationen zur Android-Befehlszeile finden Sie auf developer.android.com.

  • Weitere Informationen zu unseren Skills finden Sie in unserem GitHub-Repository.

  • In diesem Sinne: Viel Spaß beim Benchmarking und Erstellen von Benutzeroberflächen mit Jetpack Compose!

Code

Code-Snippets

Kotlin-Snippet bei 09:34 in „Managing side effects“
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
Kotlin-Snippet bei 10:15 in „Managing side effects“
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val activity = LocalActivity.current

    // Setup and teardown
    DisposableEffect(Unit) {
        activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
        onDispose {
            activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
        }
    }
}
Kotlin-Snippet bei 10:36 in „Managing side effects“
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
Kotlin-Snippet bei 11:12 in „Managing side effects“
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
Kotlin-Snippet bei 12:12 in „Managing side effects“
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
Kotlin-Snippet bei 12:36 in „Managing side effects“
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
Kotlin-Snippet bei 15:12 in „State optimization and invalidations“
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton = scrollState.value > 0
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
Kotlin-Snippet bei 15:51 in „Statusoptimierung und Entwertungen“
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
Kotlin-Snippet bei 16:49 in „Statusoptimierung und Entwertungen“
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

// First read
derivedState.value
computedState.value

// Second read
derivedState.value
computedState.value
Kotlin-Snippet bei 18:35 in „State optimization and invalidations“
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

...

// Read-after-write
a++
derivedState.value
computedState.value
Kotlin-Snippet bei 21:07 in „Skipping and stability in Compose“
// What you write
@Composable fun ContactListItem(
    contact: Contact
) { ... }

// What you get
@Composable fun ContactListItem(
    contact: Contact,
    $composer: Composer?
    $changed: Int
) {
    val $dirty = $changed or
        $composer.changed(contact)

    if ($dirty) { /* Your function body */ }
    else $composer.skipToGroupEnd()
}
Kotlin-Snippet bei 21:53 in „Skipping and stability in Compose“
data class ContactsModel(
    val header: String,
    val contacts: List<Contact>
)

data class Contact(...)

@Composable
fun ContactsScreen(model: ContactsModel) {
    Column {
        TopAppBar(title = { Text(model.header) })
        ContactsList(model.contacts)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    LazyColumn {
        items(contacts) { ContactListItem(it) }
    }
}

@Composable
fun ContactListItem(contact: Contact) { ... }
Shell-Snippet bei 29:40 in „Fast composable design and performance tools“
android skills add jetpack-compose-performance