Listen in Listen: Flutter Performance Optimierung

von Andreas Scheidmeir
Listen in Listen:  Flutter Performance Optimierung

Flutter erleichtert und beschleunigt die Implementierung von Benutzeroberflächen mit vorgefertigten Widgets, doch in manchen Fällen geraten diese an ihre Grenzen. In diesem Blogpost möchte ich schildern, wie eine neue Anforderung in einer vorhandenen App eine dieser Grenzen aufgezeigt hat und welche Lösung wir dafür gefunden haben.

Flutter Problem-Widget: List-View

Wie der Titel des Posts bereits verrät, handelt es sich bei dem Problemkind um die Implementierung von Listen in Listen. In der Ausgangssituation wurden mehrere Listen mit dem vorgefertigten Listen-Widgets List-View ineinander verschachtelt. Dabei muss sichergestellt werden, dass alle Listen zusammen als eine Einheit scrollbar sind, um das erwartete Nutzerverhalten zu gewährleisten.

Die bisherige Umsetzung erreichte dies, in dem das Widget alle Elemente berechnet und so die korrekte Höhe der Liste kennt. Diese wird zur Darstellung des Scrollbalkens und der Animation benötigt. Die konkrete Implementierung kann dem folgenden Abschnitt entnommen werden.

Diese Umsetzung funktionierte in der ursprünglichen Anforderung, da die Liste nur einmal beim Aufruf der Seite gebaut wird und nicht weiter verändert werden musste. Durch neue Anforderungen wurde der Screen um animierte Tabs erweitert, wodurch die Liste beim Wechsel ein Stottern in der Animation verursacht.

Dieses Verhalten kann man durch eine Verzögerung des Tab-Wechsels verschleiern, in dem die Animation erst gestartet wird, wenn die Liste gebaut ist. Dieser Workaround behandelt aber nur das Symptom und behebt nicht das eigentliche Problem der langsamen Liste.

Listenvergleich

Wenn man bei der Vorschau genau aufpasst, ist der zweite Übergang (zurück auf die erste, nicht optimierte Liste) leicht verzögert. Das mag im Video nicht besonders gravierend wirken, ist bei der Benutzung der App aber klar bemerkbar.  

Optimierung der Flutter Listen

Die Analyse der Debugger-Daten zeigte sehr schnell, dass der Aufbau der Liste(n) das Problem darstellt. Flutter versucht eine Bildrate von 60fps bei Animationen und Übergängen aufrecht zu erhalten, aber das Berechnen der Liste dauerte hierfür zu lange und es entsteht eine für den Nutzer deutlich spürbare Verzögerung.

Flutter Debugger: langsamer Listenaufbau

Eine neue Lösung muss also für die Listen-Implementierung her. Statt der vorgefertigten High-Level Listen Widgets müssen wir eine Ebene tiefer gehen. Dank der OpenSource-Natur von Flutter kann man unter die Haube schauen und findet, dass die Listen intern mit sogenannten Slivers aufgebaut sind. Diese lassen sich auch manuell implementieren und erreichen eine deutlich höhere Performance, in dem nur die aktuell auf dem Bildschirm vorhandenen Elemente berechnet werden müssen.

Diesen enormen Leistungsvorteil steht nur entgegen, dass die Länge der Scrollbar approximiert wird. In der Realität fällt dies dem Nutzer bei langen Listen aber nicht auf. Für technisch affine Leser möchte ich die zwei Umsetzungen im nächsten Abschnitt gegeneinanderstellen.

Listen in Scherben zerschlagen

Expanded(
  child: Scrollbar(
    child: ListView(
      children: [
        Column(
          children: const [
            Divider(),
            SizedBox(
                // Listen Header ...
                ),
            Divider(),
          ],
        ),
        ListView.builder(
          itemCount: 5,
          shrinkWrap: true,
          physics: const ClampingScrollPhysics(),
          itemBuilder: (BuildContext context, int index) {
            return Card(
              child: ListTile(
                title: Text("Erste Liste - Eintrag $index"),
              ),
            );
          },
        ),
        Column(
          children: const [
            Divider(),
            SizedBox(
                // Listen Header ...
                ),
            Divider(),
          ],
        ),
        // list part
        ListView.builder(
         // Zweite Liste ...

Die ursprüngliche Implementierung bedient sich verschachtelter ListViews. Jedes Listenteil besteht aus einem Kopfbereich, der im Column-Widget gekapselt ist, gefolgt von einem Listview.builder, welcher dynamisch eine gegebene Anzahl von Card-Elementen erstellt.

Wichtig hier sind die Attribute shrinkWrap: true und physics: ClampingScrollPhysics() an den Listview.buildern, die dafür zuständig sind, dass beide Listen als Teil des Ganzen funktionieren. ClampingScrollPhysics stellt hierbei nur sicher, dass die Liste als Ganzes scrollbar ist und nicht jede Unter-Liste einzeln. Schauen wir also auf shrinkWrap. Wenn shrinkWrap auf wahr gesetzt wird, zwingen wir die Liste nur so viel Platz einzunehmen, wie sie braucht. Der Standard false lässt sie so viel Platz einnemen, wie das Eltern-Widget zur Verfügung stellt, im Fall einer geschachtelten Liste wäre dies aber unendlich. Ohne dieses Attribut würden Nutzer also nie bis zur zweiten Liste kommen.

Hier liegt auch schon die Ursache unseres Problems: um zu wissen, wie viel Platz die Liste wirklich benötigt, muss deren Höhe exakt berechnet werden und damit wird die gesamte Liste gezeichnet.

In der optimierten Umsetzung ersetzen wir den oberen ListView durch einen CustomScrollView, welcher eine Liste von sogenannten slivers, frei übersetzt Scherben oder Stückchen, erwartet. Auf den ersten Blick mag diese Lösung komplexer erscheinen, im Kern können wir aber die meisten Elemente wieder verwenden.

Der statische Kopfbereich jeder Liste wird in einen SliverToBoxAdapter gekapselt, in den wir den vorhandenen Code einbetten können. Statt Listview.builder kommt nun eine SliverList zum Einsatz, aber auch deren Inhalt ist identisch mit dem bereits vorhandenen Code. Lediglich der Builder ist ein anderer.

Diese Änderung sieht im Nachhinein kaum anders aus als die ursprüngliche Implementierung, hat aber einen großen Unterscheid: als Resultat wird nur noch eine lange Liste erstellt, die aus vielen kleinen Stücken, den Slivers, besteht. Dadurch muss die Höhe der Liste nicht mehr zu Beginn des Screen-Aufbaus bekannt sein und kann approximiert werden. Gezeichnet wird nun nur noch das, was Nutzer auch wirklich auf dem Bildschirm angezeigt bekommen.

Den beschleunigten Listenaufbau kann man auch im Debugger validieren:

Expanded(
  child: Scrollbar(
    child: CustomScrollView(
      slivers: [
        SliverToBoxAdapter(
          child: Column(
            children: const [
              Divider(),
              SizedBox( // Listen Header ... ),
              Divider(),
            ],
          ),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return Card(
                child: ListTile( title: Text("Erste Liste - Eintrag $index"), ),
              );
            },
            childCount: 5,
          ),
        ),
        SliverToBoxAdapter(
          child: Column( // Listen Header ... ),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return // Zweite Liste
            },
            childCount: 250,
          ),
       ...
Flutter Debugger: optimierter Listenaufbau

Durch die Analyse und die Optimierung laufen Listen und Animationen jetzt nicht nur optisch flüssiger, sondern verbrauchen auf dem Endgerät weniger Rechenleistung und damit Strom, was besonders auf mobilen Geräten einen großen Mehrwert darstellt.

Mehrwert der optimierten Flutter Listen

Neue Anforderungen zeigen gelegentlich Stellen auf, die weiter optimiert werden können. Dies kann dazu führen, dass eine scheinbar simple Erweiterung mehr Aufwand erzeugt, als erwartet. Aber bei gewissenhafter Umsetzung lohnt sich dieser Aufwand, indem er das Gesamtsystem und die Nutzererfahrung verbessert. Statt einen schnellen Workaround zu implementieren, konnte durch die gezeigte Lösung nicht nur eine flüssige Oberfläche gewährleistet werden, sondern auch die Rechenlast auf dem Endgerät und damit der Stromverbrauch verringert werden. Damit profitiert auf lange Sicht nicht nur unser Kunde durch gesteigerte Nutzererfahrung und damit ein besseres Marken-Image, sondern auch deren Endnutzer durch eine schnellere und vor allem stromsparendere App.

Zurück

© 2006-2024 exensio GmbH
Einstellungen gespeichert

Datenschutzeinstellungen

Wir nutzen Cookies auf unserer Website. Einige von ihnen sind essenziell, während andere uns helfen, diese Website und Ihre Erfahrung zu verbessern.

Sie können Ihre Einwilligung jederzeit ändern oder widerrufen, indem Sie auf den Link in der Datenschutzerklärung klicken.

Zu den gesetzlichen Rechenschaftspflichten gehört die Einwilligung (Opt-In) zu protokollieren und archivieren. Aus diesem Grund wird Ihre Opt-In Entscheidung in eine LOG-Datei geschrieben. In dieser Datei werden folgende Daten gespeichert:

 

  • IP-Adresse des Besuchers
  • Vom Besucher gewählte Datenschutzeinstellung (Privacy Level)
  • Datum und Zeit des Speicherns
  • Domain
You are using an outdated browser. The website may not be displayed correctly. Close