Statusverwaltung in Flatteranwendungen


    Allgemeine Grundsätze


    Flatter ist ein reaktiver Rahmen, und für einen Entwickler, der auf native Entwicklung spezialisiert ist, kann seine Philosophie ungewöhnlich sein. Deshalb beginnen wir mit einer kleinen Besprechung.


    Die Benutzeroberfläche von Flutter besteht wie in den meisten modernen Frameworks aus einem Baum von Komponenten (Widgets). Wenn eine Komponente geändert wird, werden diese und alle ihre untergeordneten Komponenten erneut gerendert (mit internen Optimierungen, die unten beschrieben werden). Wenn sich die Anzeige global ändert (beispielsweise dreht sich der Bildschirm), wird der gesamte Baum der Widgets neu gezeichnet.


    Dieser Ansatz mag ineffizient erscheinen, aber er gibt dem Programmierer die Kontrolle über die Arbeitsgeschwindigkeit. Wenn Sie die Benutzeroberfläche auf der obersten Ebene unnötig aktualisieren, funktioniert alles langsam, aber mit dem richtigen Layout der Widgets können Anwendungen auf Flutter sehr schnell sein.


    Flutter verfügt über zwei Arten von Widgets: Statuslos und Statusvoll. Die ersten (Analoge von Pure Components in React) haben keinen Zustand und werden durch ihre Parameter vollständig beschrieben. Wenn sich die Anzeigebedingungen (z. B. die Größe des Bereichs, in dem das Widget angezeigt werden soll) und dessen Parameter nicht ändern, verwendet das System die zuvor erstellte visuelle Darstellung des Widgets erneut, sodass die Verwendung von stateless-Widgets einen guten Einfluss auf die Leistung hat. Gleichzeitig wird jedes Mal, wenn das Widget neu gezeichnet wird, formal ein neues Objekt erstellt und der Konstruktor gestartet.


    Stateful-Widgets behalten einen gewissen Zustand zwischen den Renderings. Dafür werden sie in zwei Klassen beschrieben. Die erste Klasse, das Widget selbst, beschreibt die Objekte, die mit jeder Zeichnung erstellt werden. Die zweite Klasse beschreibt den Status des Widgets und seine Objekte werden an die erstellten Widgetobjekte übertragen. Das Ändern des Stateful-Status eines Widgets ist die Hauptquelle für das Neuzeichnen von Schnittstellen. Ändern Sie dazu seine Eigenschaften innerhalb des Aufrufs der SetState-Methode. Im Gegensatz zu vielen anderen Frameworks verfügt Flutter daher nicht über eine implizite Zustandsverfolgung. Bei Änderungen der Widget-Eigenschaften außerhalb der SetState-Methode wird die Schnittstelle nicht neu gezeichnet.


    Nachdem Sie die Grundlagen beschrieben haben, können Sie mit einer einfachen Anwendung beginnen, die Stateless- und Stateful-Widgets verwendet:


    Grundanwendung
    import 'dart:math';
    import 'package:flutter/material.dart';
    void main() => runApp(new MyApp());
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return new MaterialApp(
          title: 'Flutter Demo',
          theme: new ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: Scaffold(
            appBar: AppBar(
              title: Text('Sample app'),
            ),
            body: new MyHomePage(),
          ),
        );
      }
    }
    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    class _MyHomePageState extends State<MyHomePage> {
      Random rand = Random();
      @override
      Widget build(BuildContext context) {
        return new ListView.builder(itemBuilder: (BuildContext context, int index) {
          return Text('Random number ${rand.nextInt(100)}',);
        });
      }
    }

    Vollständiges Beispiel


    Das Ergebnis


    Wenn Sie dauerhaftere Zustände benötigen


    Mach weiter. Der stateful-Status von Widgets wird zwischen dem Neuzeichnen der Schnittstelle beibehalten, jedoch nur, solange das Widget benötigt wird, d. H. ist wirklich auf dem Bildschirm. Lassen Sie uns ein einfaches Experiment machen - wir werden unsere Liste auf der Registerkarte platzieren:


    Anwendung mit Registern
    class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
      Random rand = Random();
      TabController _tabController;
      final List<Tab> myTabs = <Tab>[
        new Tab(text: 'FIRST'),
        new Tab(text: 'SECOND'),
      ];
      @override
      void initState() {
        super.initState();
        _tabController = new TabController(vsync: this, length: myTabs.length);
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
            title: Text('Sample app'),
        ),
        body: new TabBarView(
            controller: _tabController,
            children: [
              new ListView.builder(itemBuilder: (BuildContext context, int index) {
                return Text('Random number ${rand.nextInt(100)}',);
              }),
              Text('Second tab'),
            ],),
          bottomNavigationBar: new TabBar(
            controller: _tabController,
            tabs: myTabs,
            labelColor: Colors.blue,
          ),
        );
      }
    }

    Vollständiges Beispiel


    Das Ergebnis


    Beim Start sehen Sie, dass beim Wechseln zwischen Registerkarten der Status gelöscht wird (die dispose () -Methode wird aufgerufen), und wenn sie zurückgegeben wird, wird sie erneut erstellt (die initState () -Methode). Dies ist sinnvoll, da durch das Speichern des Status von nicht darstellbaren Widgets Systemressourcen wegfallen. In dem Fall, in dem der Zustand des Widget vollständig ausgeblendet werden sollte, sind mehrere Ansätze möglich:


    Zunächst können Sie separate Objekte (ViewModel) verwenden, um den Status zu speichern. Die Dart-Level-Sprache unterstützt Factory-Konstruktoren, mit denen Fabriken und Singleltons erstellt werden können, in denen die erforderlichen Daten gespeichert werden.


    Ich bevorzuge diesen Ansatz, weil Sie können die Geschäftslogik von der Benutzeroberfläche isolieren. Dies ist vor allem auf die Tatsache zurückzuführen, dass Flutter Release Preview 2 die Möglichkeit bietet, pixelgenaue Schnittstellen für iOS zu erstellen, dies sollte jedoch natürlich auf den entsprechenden Widgets erfolgen.


    Zweitens können Sie den State-Raising-Ansatz verwenden, der den Programmierern von React bekannt ist, wenn Daten in Komponenten gespeichert werden, die sich weiter oben in der Baumstruktur befinden. Da Flutter die Schnittstelle nur beim Aufruf der setState () -Methode neu zeichnet, können diese Daten geändert und ohne Rendering verwendet werden. Dieser Ansatz ist etwas komplexer und erhöht die Konnektivität der Widgets in der Struktur. Sie können jedoch die Ebene der Datenspeicherung angeben.


    Schließlich gibt es Zustandsspeicherbibliotheken, zum Beispiel flutter_redux .


    Der Einfachheit halber verwenden wir den ersten Ansatz. Lassen Sie uns eine separate Klasse ListData, Singleton, erstellen, die Werte für unsere Liste speichert. Beim Mapping verwenden wir diese Klasse.


    Datenwiederherstellungsanwendung mit Registerkarten
    class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
      TabController _tabController;
      final List<Tab> myTabs = <Tab>[
        new Tab(text: 'FIRST'),
        new Tab(text: 'SECOND'),
      ];
      @override
      void initState() {
        super.initState();
        _tabController = new TabController(vsync: this, length: myTabs.length);
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
            title: Text('Sample app'),
        ),
        body: new TabBarView(
            controller: _tabController,
            children: [
              new ListView.builder(itemBuilder: ListData().build),
              Text('Second tab'),
            ],),
          bottomNavigationBar: new TabBar(
            controller: _tabController,
            tabs: myTabs,
            labelColor: Colors.blue,
          ),
        );
      }
    }
    class ListData {
      static ListData _instance = ListData._internal();
      ListData._internal();
      factory ListData() {
        return _instance;
      }
      Random _rand = Random();
      Map<int, int> _values = new Map();
      Widget build (BuildContext context, int index) {
        if (!_values.containsKey(index)) {
          _values[index] = _rand.nextInt(100);
        }
        return Text('Random number ${_values[index]}',);
      }
    }

    Vollständiges Beispiel


    Das Ergebnis


    Bildlaufposition speichern


    Wenn Sie die Liste aus dem vorherigen Beispiel verdrehen und dann zwischen den Registerkarten wechseln, können Sie leicht erkennen, dass die Bildlaufposition nicht gespeichert wird. Dies ist logisch, da es nicht in unserer ListData-Klasse gespeichert wird und der eigene Status des Widget nicht zwischen Registerkarten wechseln kann. Wir implementieren die Speicherung des Bildlaufzustands manuell, fügen sie jedoch aus Gründen des Interesses nicht in einer separaten Klasse oder ListData ein, sondern in einem übergeordneten Status, um zu zeigen, wie damit gearbeitet wird.


    Beachten Sie die Widgets ScrollController und NotificationListener (sowie den zuvor verwendeten DefaultTabController). Das Konzept von Widgets, die keine eigene Anzeige haben, sollte Entwicklern, die mit React / Redux arbeiten, vertraut sein - in diesem Bündel werden Container-Komponenten aktiv verwendet. In Flutter werden normalerweise nicht zugeordnete Widgets verwendet, um untergeordneten Widgets Funktionen hinzuzufügen. Auf diese Weise können Sie die visuellen Widgets selbst klein halten und keine Systemereignisse behandeln, bei denen sie nicht benötigt werden.


    Der Code basiert auf der von Marcin Szałek bei Stakoverflow vorgeschlagenen Lösung ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ). Der Plan ist:


    1. Fügen Sie der Liste einen ScrollController hinzu, um mit der Bildlaufposition zu arbeiten.
    2. Fügen Sie der Liste einen NotificationListener hinzu, um den Bildlaufstatus anzuzeigen.
    3. Speichern Sie die Position der Bildlaufleiste in _MyHomePageState (die sich oberhalb der Registerkarten befindet) und ordnen Sie sie der Bildlaufliste zu.

    Anwendung unter Beibehaltung der Bildlaufposition
    class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
      double listViewOffset=0.0;
      TabController _tabController;
      final List<Tab> myTabs = <Tab>[
        new Tab(text: 'FIRST'),
        new Tab(text: 'SECOND'),
      ];
      @override
      void initState() {
        super.initState();
        _tabController = new TabController(vsync: this, length: myTabs.length);
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
            title: Text('Sample app'),
        ),
        body: new TabBarView(
            controller: _tabController,
            children: [new ListTab(
              getOffsetMethod: () => listViewOffset,
              setOffsetMethod: (offset) => this.listViewOffset = offset,
            ),
              Text('Second tab'),
            ],),
          bottomNavigationBar: new TabBar(
            controller: _tabController,
            tabs: myTabs,
            labelColor: Colors.blue,
          ),
        );
      }
    }
    class ListTab extends StatefulWidget {
      ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key);
      final GetOffsetMethod getOffsetMethod;
      final SetOffsetMethod setOffsetMethod;
      @override
      _ListTabState createState() => _ListTabState();
    }
    class _ListTabState extends State<ListTab> {
      ScrollController scrollController;
      @override
      void initState() {
        super.initState();
        //Init scrolling to preserve it
        scrollController = new ScrollController(
            initialScrollOffset: widget.getOffsetMethod()
        );
      }
      @override
      Widget build(BuildContext context) {
        return
          NotificationListener(
            child: new ListView.builder(
              controller: scrollController,
              itemBuilder: ListData().build,
            ),
            onNotification: (notification) {
              if (notification is ScrollNotification) {
                widget.setOffsetMethod(notification.metrics.pixels);
              }
            },
          );
      }
    }

    Vollständiges Beispiel


    Das Ergebnis


    Wir erleben das Herunterfahren der Anwendung


    Das Speichern von Informationen zum Zeitpunkt der Anwendung ist gut. Häufig möchten Sie jedoch, dass die Informationen zwischen den Sitzungen gespeichert werden. Dies gilt insbesondere für die Angewohnheit der Betriebssysteme, Hintergrundanwendungen zu schließen, wenn der Speicher knapp wird. Die Hauptoptionen für die permanente Speicherung von Daten in Flutter sind:


    1. Shared Preferences ( https://pub.dartlang.org/packages/shared_preferences ) ist ein Wrapper um NSUserDefaults (unter iOS) und SharedPreferences (unter Android) und ermöglicht das Speichern einer kleinen Anzahl von Schlüsselwertpaaren. Ideal zum Speichern von Einstellungen.
    2. sqflite ( https://pub.dartlang.org/packages/sqflite ) ist ein Plugin für die Arbeit mit SQLite (mit einigen Einschränkungen). Unterstützt sowohl einfache Abfragen als auch Helfer. Analog zu Room können Sie außerdem mit Versionen des Datenbankschemas arbeiten und den Code für die Aktualisierung des Schemas beim Aktualisieren der Anwendung festlegen.
    3. Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) ist Teil der FireBase-Familie der offiziellen Plugins.

    Für die Demonstration wird der Bildlaufstatus in den gemeinsamen Einstellungen beibehalten. Dazu fügen wir die Wiederherstellung der Position der Bildlaufleiste hinzu, wenn der Status _MyHomePageState initialisiert und beim Bildlauf gespeichert wird.


    Hier müssen Sie auf das asynchrone Flutter / Dart-Modell eingehen, da alle externen Dienste mit asynchronen Aufrufen ausgeführt werden. Das Funktionsprinzip dieses Modells ähnelt dem von node.js: Es gibt einen Hauptausführungsthread (Thread), der durch asynchrone Aufrufe unterbrochen wird. Bei jedem nachfolgenden Interrupt (und die UI macht sie ständig), werden die Ergebnisse abgeschlossener asynchroner Vorgänge verarbeitet. Gleichzeitig können in Hintergrundthreads (über die Berechnungsfunktion) Berechnungen mit hohem Gewicht ausgeführt werden.


    Das Schreiben und Lesen in SharedPreferences erfolgt also asynchron (obwohl die Bibliothek das synchrone Lesen aus dem Cache ermöglicht). Zuerst beschäftigen wir uns mit dem Lesen. Der Standardansatz für das asynchrone Abrufen von Daten sieht folgendermaßen aus: Starten Sie einen asynchronen Prozess. Wenn der Vorgang abgeschlossen ist, führen Sie SetState aus, und schreiben Sie die erhaltenen Werte. Als Ergebnis wird die Benutzeroberfläche unter Verwendung der empfangenen Daten aktualisiert. In diesem Fall arbeiten wir jedoch nicht mit den Daten, sondern mit der Bildlaufposition. Wir müssen die Schnittstelle nicht aktualisieren, wir müssen lediglich die JumpTo-Methode ScrollController aufrufen. Das Problem ist, dass das Ergebnis der Verarbeitung einer asynchronen Anforderung jederzeit zurückgegeben werden kann und es überhaupt nicht notwendig ist, was und wo gescrollt werden soll. Um sicherzustellen, dass die Operation auf einer vollständig initialisierten Schnittstelle ausgeführt wird, müssen wir noch im setState scrollen.


    Wir bekommen so etwas:


    Zustand einstellen
      @override
      void initState() {
        super.initState();
        //Init scrolling to preserve it
        scrollController = new ScrollController(
            initialScrollOffset: widget.getOffsetMethod()
        );
        _restoreState().then((double value) => scrollController.jumpTo(value));
      }
      Future<double> _restoreState() async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        return prefs.getDouble('listViewOffset');
      }
      void setScroll(double value) {
        setState(() {
          scrollController.jumpTo(value);
        });
    }

    Die Aufnahme ist interessanter. Tatsache ist, dass während des Scrollens die Ereignisse, die darüber berichten, ständig auftreten. Das Starten eines asynchronen Schreibens bei jeder Änderung des Werts kann zu Anwendungsfehlern führen. Wir müssen nur das letzte Ereignis der Kette abwickeln. In Bezug auf reaktive Programmierung heißt das Debounce und wir werden es verwenden. Dart unterstützt die grundlegenden Funktionen der reaktiven Programmierung über Datenströme (Stream). Wir müssen aus den Aktualisierungen der Bildlaufposition einen Stream erstellen und ihn abonnieren und ihn mit Debounce umwandeln. Für die Konvertierung benötigen wir die stream_transform-Bibliothek . Alternativ können Sie RxDart verwenden und in Bezug auf ReactiveX arbeiten.


    Ein solcher Code stellt sich heraus:


    Zustandseintrag
      StreamSubscription _stream;
      StreamController<double> _controller = new StreamController<double>.broadcast();
      @override
      void initState() {
        super.initState();
        _tabController = new TabController(vsync: this, length: myTabs.length);
        _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState);
      }
      void _saveState(double value) async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        await prefs.setDouble('listViewOffset', value);
    }

    Vollständiges Beispiel


    Jetzt auch beliebt: