
Allgemeine Grundsätze
Flutter ist ein reaktives Framework, und für einen Entwickler, der sich auf native Entwicklung spezialisiert hat, kann seine Philosophie ungewöhnlich sein. Daher beginnen wir mit einem kurzen Rückblick.
Die Benutzeroberfläche von Flutter besteht wie in den meisten modernen Frameworks aus einem Baum von Komponenten (Widgets). Wenn sich eine Komponente ändert, werden diese und alle untergeordneten Komponenten neu gerendert (mit internen Optimierungen, die unten beschrieben werden). Wenn sich die Anzeige global ändert (z. B. durch Drehen des Bildschirms), wird der gesamte Widget-Baum neu gezeichnet.
Dieser Ansatz mag unwirksam erscheinen, gibt dem Programmierer jedoch die Kontrolle über die Arbeitsgeschwindigkeit. Wenn Sie die Benutzeroberfläche auf höchster Ebene aktualisieren, ohne dass dies erforderlich ist, funktioniert alles langsam. Mit dem richtigen Layout der Widgets können Anwendungen auf Flutter jedoch sehr schnell ausgeführt werden.
Flutter hat zwei Arten von Widgets - Stateless und Stateful. Die ersteren (analog zu 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 seine Parameter nicht ändern, verwendet das System die zuvor erstellte visuelle Darstellung des Widgets erneut, sodass sich die Verwendung zustandsloser Widgets positiv auf die Leistung auswirkt. Gleichzeitig wird jedes Mal, wenn das Widget neu gezeichnet wird, ein neues Objekt formell erstellt und der Konstruktor gestartet.
Stateful-Widgets behalten einen gewissen Status zwischen den Renderings bei. Dazu werden sie von zwei Klassen beschrieben. Die erste der Klassen, das Widget selbst, beschreibt die Objekte, die bei jedem Rendern erstellt werden. Die zweite Klasse beschreibt den Status des Widgets und seine Objekte werden auf die erstellten Widget-Objekte übertragen. Stateful State Widgets sind eine wichtige Quelle für das Neuzeichnen von Schnittstellen. Dazu müssen Sie die Eigenschaften innerhalb des Aufrufs der SetState-Methode ändern. Im Gegensatz zu vielen anderen Frameworks verfügt Flutter daher nicht über eine implizite Statusverfolgung. Eine Änderung der Eigenschaften des Widgets außerhalb der SetState-Methode führt nicht zu einem erneuten Zeichnen der Schnittstelle.
Nachdem Sie die Grundlagen beschrieben haben, können Sie mit einer einfachen Anwendung beginnen, die Stateless- und Stateful-Widgets verwendet:
Basisanwendungimport '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
Wenn Sie hartnäckigere Bedingungen benötigen
Lass uns weitermachen. Der Status der Widgets wird zwischen dem erneuten Zeichnen von Schnittstellen beibehalten, jedoch nur solange das Widget benötigt wird, d. H. wirklich auf dem Bildschirm gelegen. Lassen Sie uns ein einfaches Experiment durchführen - platzieren Sie unsere Liste auf der Registerkarte:
Tab App 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
Beim Start können Sie sehen, dass beim Wechseln zwischen Registerkarten der Status gelöscht wird (die Methode dispose () wird aufgerufen) und bei der Rückgabe erneut erstellt wird (die Methode initState ()). Dies ist sinnvoll, da das Speichern des Status nicht anzeigbarer Widgets Systemressourcen verbraucht. Für den Fall, dass der Status des Widgets seine vollständige Verschleierung überleben muss, sind mehrere Ansätze möglich:
Zunächst können Sie separate Objekte (ViewModel) zum Speichern des Status verwenden. Dart auf Sprachebene unterstützt Factory-Konstruktoren, mit denen Fabriken und Singletones erstellt werden können, in denen die erforderlichen Daten gespeichert werden.
Ich mag diesen Ansatz mehr, weil Sie können damit die Geschäftslogik von der Benutzeroberfläche isolieren. Dies gilt insbesondere aufgrund der Tatsache, dass Flutter Release Preview 2 die Möglichkeit hinzugefügt hat, pixelgenaue Schnittstellen für iOS zu erstellen. Dies müssen Sie jedoch natürlich für die entsprechenden Widgets tun.
Zweitens ist es möglich, den den React-Programmierern bekannten State-Raising-Ansatz zu verwenden, wenn Daten in vorgelagerten Komponenten gespeichert werden. Da Flutter die Schnittstelle nur beim Aufruf der Methode setState () neu zeichnet, können diese Daten ohne Rendern geändert und verwendet werden. Dieser Ansatz ist etwas komplexer und erhöht die Konnektivität von Widgets in der Struktur. Sie können jedoch die Ebene der Datenspeicherung punktuell angeben.
Schließlich gibt es Statusspeicherbibliotheken wie flutter_redux .
Der Einfachheit halber verwenden wir den ersten Ansatz. Erstellen wir eine separate ListData-Klasse, Singleton, in der die Werte für unsere Liste gespeichert werden. Bei der Anzeige verwenden wir diese Klasse.
Registerkarten-Datenwiederherstellungsanwendung 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
Speichern einer Bildlaufposition
Wenn Sie im vorherigen Beispiel in der Liste nach unten scrollen und dann zwischen den Registerkarten wechseln, ist leicht zu erkennen, dass die Bildlaufposition nicht gespeichert ist. Dies ist logisch, da es nicht in unserer ListData-Klasse gespeichert ist und der Status des Widgets den Wechsel zwischen Registerkarten nicht überlebt. Wir implementieren den Speicher für den Bildlaufstatus manuell, fügen ihn jedoch zum Spaß nicht einer separaten Klasse und nicht ListData hinzu, sondern einem Status höherer Ebene, um zu zeigen, wie damit gearbeitet wird.
Beachten Sie die Widgets ScrollController und NotificationListener (sowie den zuvor verwendeten DefaultTabController). Das Konzept von Widgets ohne eigene Anzeige sollte Entwicklern, die mit React / Redux arbeiten, vertraut sein - Containerkomponenten werden in diesem Bundle aktiv verwendet. In Flutter werden häufig Widgets ohne Anzeige verwendet, um untergeordneten Widgets Funktionen hinzuzufügen. Auf diese Weise können Sie die visuellen Widgets selbst leicht machen und Systemereignisse nicht dort verarbeiten, wo 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 wie folgt:
- Fügen Sie der Liste einen ScrollController hinzu, um mit der Bildlaufposition zu arbeiten.
- Fügen Sie der Liste NotificationListener hinzu, um den Bildlaufstatus zu übergeben.
- Wir speichern die Bildlaufposition in _MyHomePageState (eine Ebene über den Registerkarten) und ordnen sie dem Listenlauf zu.
Anwendung mit gespeicherter 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
Herunterfahren der Anwendung
Das Speichern von Informationen für die Dauer der Anwendung ist gut, aber häufig möchten Sie sie zwischen Sitzungen speichern, insbesondere angesichts der Gewohnheit von Betriebssystemen, Hintergrundanwendungen zu schließen, wenn nicht genügend Speicher vorhanden ist. Die Hauptoptionen für die dauerhafte Datenspeicherung in Flutter sind:
- Shared Preferences ( https://pub.dartlang.org/packages/shared_preferences ) ist ein Wrapper für NSUserDefaults (unter iOS) und SharedPreferences (unter Android) und ermöglicht das Speichern einer kleinen Anzahl von Schlüssel-Wert-Paaren. Ideal zum Speichern von Einstellungen.
- sqflite ( https://pub.dartlang.org/packages/sqflite ) ist ein Plugin für die Arbeit mit SQLite (mit einigen Einschränkungen). Unterstützt sowohl Abfragen auf niedriger Ebene als auch Helfer. In Analogie zu Room können Sie außerdem mit Datenbankschemaversionen arbeiten und den Code für die Aktualisierung des Schemas beim Aktualisieren der Anwendung festlegen.
- Der Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) ist Teil der offiziellen FireBase-Plugin-Familie.
Zur Demonstration speichern wir den Bildlaufstatus in den freigegebenen Einstellungen. Fügen Sie dazu die Wiederherstellung der Bildlaufposition hinzu, wenn Sie den Status _MyHomePageState initialisieren und beim Bildlauf speichern.
Hier müssen wir uns mit dem asynchronen Flutter / Dart-Modell befassen, da alle externen Dienste mit asynchronen Aufrufen arbeiten. 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 Benutzeroberfläche macht sie ständig) werden die Ergebnisse abgeschlossener asynchroner Operationen verarbeitet. Gleichzeitig ist es möglich, umfangreiche Berechnungen in Hintergrundthreads auszuführen (über die Berechnungsfunktion).
Das Schreiben und Lesen in SharedPreferences erfolgt also asynchron (obwohl die Bibliothek das synchrone Lesen aus dem Cache ermöglicht). Zunächst werden wir uns mit dem Lesen befassen. Der Standardansatz für den asynchronen Datenabruf sieht folgendermaßen aus: Starten Sie den asynchronen Prozess und führen Sie nach Abschluss SetState aus, indem Sie die empfangenen Werte schreiben. Infolgedessen wird die Benutzeroberfläche anhand der empfangenen Daten aktualisiert. In diesem Fall arbeiten wir jedoch nicht mit Daten, sondern mit der Bildlaufposition. Wir müssen die Schnittstelle nicht aktualisieren, wir müssen nur die jumpTo-Methode auf dem ScrollController aufrufen. Das Problem ist, dass das Ergebnis der Verarbeitung einer asynchronen Anforderung jederzeit zurückgegeben werden kann und es nicht unbedingt sein muss, was und wo gescrollt werden soll. Um sicherzustellen, dass eine Operation an einer vollständig initialisierten Schnittstelle ausgeführt wird, müssen wir ... immer noch in setState scrollen.
Wir bekommen so etwas wie diesen Code:
Statuseinstellung @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); }); }
Mit der Aufzeichnung ist alles interessanter. Tatsache ist, dass beim Scrollen ständig Ereignisse auftreten, die darüber berichten. Das Starten der asynchronen Aufzeichnung bei jeder Änderung des Werts kann zu Anwendungsfehlern führen. Wir müssen nur das letzte Ereignis aus der Kette verarbeiten. In Bezug auf die reaktive Programmierung wird dies als Entprellen bezeichnet und wir werden es verwenden. Dart unterstützt die Hauptfunktionen der reaktiven Programmierung über Datenströme. Daher müssen wir einen Stream aus Aktualisierungen der Bildlaufposition erstellen, ihn abonnieren und mithilfe von Debounce konvertieren. Zum Konvertieren benötigen wir die Bibliothek stream_transform . Alternativ können Sie RxDart verwenden und in Bezug auf ReactiveX arbeiten.
Es stellt sich folgender Code heraus:
Statusdatensatz 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