Details zum Provider-Paket für Flutter

Hallo Habr!

Unsere mittelfristigen Pläne beinhalten die Veröffentlichung des Flutter-Buches. In Bezug auf die Sprache von Dart als Thema nehmen wir immer noch eine vorsichtigere Position ein, daher werden wir versuchen, ihre Relevanz anhand der Ergebnisse dieses Artikels zu bewerten. Es wird sich auf das Provider- Paket und damit auf das State Management in Flutter konzentrieren.

Provider ist ein State-Management-Paket, das von Remy Rusle geschrieben und von Google und der Flutter-Community übernommen wurde. Aber was ist Staatsverwaltung? Was ist für den Anfang eine Bedingung? Ich möchte Sie daran erinnern, dass der Status nur Daten zur Darstellung der Benutzeroberfläche in Ihrer Anwendung sind. Das staatliche Management ist ein Ansatz zur Erstellung, zum Zugriff, zur Verarbeitung und zur Entsorgung dieser Daten. Um das Provider-Paket besser zu verstehen, skizzieren wir kurz die Geschichte der staatlichen Verwaltung in Flutter.

1. StatefulWidget


StatelessWidget ist eine einfache UI-Komponente, die nur angezeigt wird, wenn sie Daten enthält. Es gibt StatelessWidgetkeine "Erinnerung"; es wird nach Bedarf erstellt und zerstört. Flutter hat auch ein StatefulWidget , in dem sich ein Speicher befindet, dank dessen ein langlebiger Satellit - das State- Objekt . Diese Klasse verfügt über eine Methode setState(). Wenn sie aufgerufen wird, wird ein Widget gestartet, das den Status neu erstellt und in einer neuen Form anzeigt. Dies ist die einfachste Form der sofort verfügbaren Flatter-Statusverwaltung. Hier ist ein Beispiel mit einer Schaltfläche, die immer die Uhrzeit anzeigt, zu der sie zuletzt gedrückt wurde:

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

Was ist das Problem bei diesem Ansatz? Angenommen, in Ihrer Anwendung ist ein globaler Status im Stammverzeichnis gespeichert StatefulWidget. Es enthält Daten, die zur Verwendung in verschiedenen Teilen der Benutzeroberfläche vorgesehen sind. Diese Daten werden gemeinsam genutzt und in Form von Parametern an jedes untergeordnete Widget übergeben. Alle Ereignisse, bei denen geplant ist, diese Daten zu ändern, werden in Form von Rückrufen angezeigt. Somit werden über alle Zwischen-Widgets viele Parameter und Rückrufe übertragen, was bald zu Verwirrung führen kann. Schlimmer noch, alle Aktualisierungen des oben genannten Stammverzeichnisses führen zu einer Neuerstellung des gesamten Widget-Baums, was ineffizient ist.

2. InheritedWidget


InheritedWidget ist ein spezielles Widget, auf dessen Nachkommen ohne direkten Link zugegriffen werden kann. InheritedWidgetWenn Sie sich an wenden, kann sich ein konsumierendes Widget für eine automatische Neuerstellung registrieren, die beim Neuerstellen eines Ahnen-Widgets auftritt. Mit dieser Technik können Sie die Aktualisierung der Benutzeroberfläche effizienter organisieren. Anstatt große Teile der Anwendung als Reaktion auf eine kleine Statusänderung neu zu erstellen, können Sie selektiv nur die spezifischen Widgets auswählen, die neu erstellt werden müssen. Sie haben bereits mit gearbeitet, InheritedWidgetwann immer Sie MediaQuery.of(context)oder verwendet haben Theme.of(context). Es ist zwar weniger wahrscheinlich, dass Sie Ihr eigenes InheritedWidget mit Statuserhaltung implementiert haben. Tatsache ist, dass die korrekte Implementierung nicht einfach ist.

3. ScopedModel


ScopedModel ist ein 2017 von Brian Egan erstelltes Paket, mit dem sich der InheritedWidgetAnwendungsstatus einfach speichern lässt. Zuerst müssen Sie ein Statusobjekt erstellen, das von Model erbt , und es dann aufrufen notifyListeners(), wenn sich seine Eigenschaften ändern. Die Situation erinnert an die Implementierung der PropertyChangeListener- Schnittstelle in Java.

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Um unser Statusobjekt bereitzustellen, verpacken wir dieses Objekt in ein Widget ScopedModelim Stammverzeichnis unserer Anwendung:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

Jetzt können alle Nachkommen-Widgets MyModelüber das ScopedModelDescendant- Widget darauf zugreifen . Die Modellinstanz wird an den Parameter übergeben builder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

Jedes untergeordnete Widget kann das Modell auch aktualisieren, wodurch automatisch eine Neuerstellung eines ScopedModelDescendantsModells ausgelöst wird (vorausgesetzt, unser Modell ruft es korrekt auf notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModelhat in Flutter als Werkzeug für die Zustandsverwaltung an Popularität gewonnen, seine Verwendung beschränkt sich jedoch auf die Bereitstellung von Objekten, die die Klasse erben Modelund dieses Muster der Benachrichtigung über Änderungen verwenden.

4. BLoC


Auf der Google I / O '18 -Konferenz wurde das BLoC- Muster ( Business Logic Component ) vorgestellt , das als weiteres Tool zum Abrufen des Status von Widgets dient. BLoC-Klassen sind langlebige Nicht-UI-Komponenten, die den Status beibehalten und als Streams und Empfänger verfügbar machen. Wenn Sie StatelessWidgetdie Status- und Geschäftslogik über die Benutzeroberfläche hinaus verwenden, können Sie das Widget so einfach wie möglich implementieren und StreamBuilder für die automatische Neuerstellung verwenden . Infolgedessen wird das Widget "dumm" und es wird einfacher zu testen.

Beispiel BLoC-Klasse:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

Das Problem mit dem BLoC-Muster ist, dass es nicht offensichtlich ist, wie BLoC-Objekte erstellt und zerstört werden. Wie wurde die Instanz myBlocim obigen Beispiel erstellt? Wie rufen wir dispose()an, um ihn loszuwerden? Streams müssen verwendet StreamControllerwerden. closedDies sollte geschehen, sobald dies nicht mehr erforderlich ist. Dies geschieht, um Speicherverluste zu vermeiden. (In Dart gibt es keinen Klassendestruktor; nur eine Klasse Statein StatefulWidgethat eine Methode dispose()). Darüber hinaus ist nicht klar, wie dieses BLoC für mehrere Widgets freigegeben werden soll. Für Entwickler ist es oft schwierig, BLoC zu beherrschen. Es gibt mehrere Pakete, die versuchen, dies zu vereinfachen.

5. Anbieter


ProviderIst ein Paket, das 2018 von Remy Rusle geschrieben wurde, ähnlich ScopedModel, aber dessen Funktionen nicht darauf beschränkt sind, eine Unterklasse von Model bereitzustellen. Dies ist auch ein abschließender Wrapper InheritedWidget, aber der Anbieter kann alle Statusobjekte bereitstellen, einschließlich BLoC, Streams, Futures und andere. Da der Anbieter so einfach und flexibel ist, gab Google auf der Google I / O '19 -Konferenz bekannt, dass er in Zukunft Providerdas bevorzugte Paket für die Verwaltung des Status sein wird. Natürlich sind auch andere Pakete zulässig. Wenn Sie jedoch Zweifel haben, empfiehlt Google, einen Zwischenstopp einzulegen Provider.

Providergebaut "mit Widgets, für Widgets."ProviderMit dieser Option können Sie jedes Objekt mit einem Status in den Widget-Baum einfügen und den Zugriff darauf für jedes andere Widget (untergeordnetes Element) öffnen. Außerdem können Sie Providerdie Lebensdauer von Statusobjekten verwalten, indem Sie sie mit Daten initialisieren und eine Bereinigung durchführen, nachdem sie aus dem Widget-Baum entfernt wurden. Daher Providereignet es sich auch zur Implementierung von BLoC-Komponenten oder kann als Basis für andere State-Management-Lösungen dienen! Oder einfach zum Implementieren von Abhängigkeiten verwendet - ein ausgefallener Begriff, bei dem Daten auf eine Weise an Widgets übertragen werden, mit der Sie die Verbindung lösen und die Testbarkeit des Codes verbessern können. Schließlich,Providerkommt mit einer Reihe von speziellen Klassen, dank denen es noch bequemer zu bedienen ist. Als nächstes werden wir uns jede dieser Klassen genauer ansehen.

  • Basisanbieter
  • ChangeNotifierProvider
  • StreamProvider
  • Zukunftsanbieter
  • ValueListenableProvider
  • MultiProvider
  • Proxyprovider

Installation


Um es zu verwenden Provider, fügen Sie zuerst eine Abhängigkeit zu unserer Datei hinzu pubspec.yaml:

provider: ^3.0.0

Dann importieren wir das Paket dort, Providerwo es benötigt wird:

import 'package:provider/provider.dart';

Basisanbieter

Erstellen Sie die Basis Provider im Stammverzeichnis unserer Anwendung; Dies wird eine Instanz unseres Modells enthalten:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

Der Parameter buildererstellt eine Instanz MyModel. Wenn Sie eine vorhandene Instanz an diese übergeben möchten, verwenden Sie hier den Konstruktor Provider.value.

Anschließend können Sie diese Instanz des Modells MyAppmithilfe des Widgets an einer beliebigen Stelle verwenden Consumer:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

Im obigen Beispiel MyWidgeterhält die Klasse eine Instanz MyModelmithilfe des Consumer- Widgets . Dieses Widget gibt uns builderdie Möglichkeit , unser Objekt im Parameter zu enthalten value.

Was sollen wir nun tun, wenn wir die Daten in unserem Modell aktualisieren möchten? Angenommen, wir haben ein anderes Widget, bei dem beim Klicken auf eine Schaltfläche die Eigenschaft aktualisiert werden sollte foo:

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

Beachten Sie die spezifische Syntax für den Zugriff auf die Instanz MyModel. Funktionell entspricht dies dem Zugriff auf das Widget Consumer. Das Widget Consumerist nützlich, wenn der Code den Link nicht einfach abrufen kann BuildContext.

Was wird Ihrer Meinung nach mit dem ursprünglichen Widget passieren, das MyWidgetwir zuvor erstellt haben? Wird darin eine neue Bedeutung angezeigt bar? Leider nein . Es ist nicht möglich, Änderungen an alten traditionellen Dart-Objekten zu hören (zumindest ohne Reflexion, die in Flutter nicht vorgesehen ist). Daher kann Provideres nicht "sehen", dass wir die Eigenschaft ordnungsgemäß aktualisiert foound angeordnet haben, dass das Widget als MyWidgetAntwort aktualisiert wird.

ChangeNotifierProvider

Aber es gibt Hoffnung! Sie können unsere Klasse dazu bringen MyModel, eine Verunreinigung zu implementieren ChangeNotifier. Es wird einige Zeit dauern, die Implementierung unseres Modells zu ändern und eine spezielle Methode aufzurufen, notifyListeners()wenn sich eine unserer Eigenschaften ändert. Es funktioniert ungefähr genauso ScopedModel, aber in diesem Fall ist es schön, dass Sie nicht von einer bestimmten Klasse des Modells erben müssen. Es reicht aus, die Beimischung zu realisieren ChangeNotifier. So sieht es aus:

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Wie Sie sehen können, haben wir unsere Eigenschaft foodurch getterund ersetzt setter, unterstützt durch die private Variable _foo. Auf diese Weise können wir alle an der foo-Eigenschaft vorgenommenen Änderungen „abfangen“ und unseren Listenern mitteilen, dass sich unser Objekt geändert hat.

Jetzt Providerkönnen wir unsere Implementierung von außen so ändern, dass eine andere Klasse namens ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

So! Wenn nun unsere OtherWidgetUpdates Eigentum fooin der Instanz MyModel, es MyWidgetwird automatisch aktualisiert , um diese Änderung widerzuspiegeln. Cool, oder?

Apropos. Sie haben wahrscheinlich einen Button-Handler bemerkt, OtherWidgetmit dem wir die folgende Syntax verwendet haben:

final model = Provider.of<MyModel>(context);

Standardmäßig führt diese Syntax automatisch zu einer Neuerstellung der Instanz, OtherWidgetsobald sich das Modell ändert MyModel. Vielleicht brauchen wir das nicht. Am Ende OtherWidgetenthält es einfach eine Schaltfläche, die sich überhaupt nicht ändert, wenn sich der Wert ändert MyModel. Um eine Neuerstellung zu vermeiden, können Sie die folgende Syntax verwenden, um auf unser Modell zuzugreifen, ohne sich für eine Neuerstellung zu registrieren:

final model = Provider.of<MyModel>(context, listen: false);

Dies ist ein weiterer Charme, der Providereinfach so im Paket enthalten ist .

StreamProvider

Auf den ersten Blick ist nicht klar, warum es benötigt wird StreamProvider. Am Ende können Sie nur das Übliche verwenden, StreamBuilderwenn Sie einen Stream in Flutter verbrauchen müssen. Hier hören wir zum Beispiel den Stream onAuthStateChangedvon FirebaseAuth:

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

Um dasselbe mit Hilfe zu tun Provider, können wir unseren Stream StreamProviderim Stammverzeichnis unserer Anwendung bereitstellen:

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

Verwenden Sie dann das untergeordnete Widget, wie dies normalerweise der Fall ist Provider:

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

Unser Widget-Code ist nicht nur viel sauberer geworden, sondern abstrahiert auch die Tatsache, dass die Daten aus dem Stream stammen. Wenn wir uns jemals dazu entschließen, die Basisimplementierung beispielsweise auf zu ändern FutureProvider, sind keine Änderungen am Widget-Code erforderlich. Wie Sie sehen werden, gilt dies für alle anderen unten aufgeführten Anbieter.

FutureProvider

Ähnlich wie im obigen Beispiel ist es FutureProvidereine Alternative zum Standard, FutureBuilderwenn Sie mit Widgets arbeiten. Hier ist ein Beispiel:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

Um diesen Wert im untergeordneten Widget zu verwenden, verwenden wir dieselbe Implementierung Consumerwie im StreamProviderobigen Beispiel .

ValueListenableProvider

ValueListenable ist eine von der ValueNotifier- Klasse implementierte Dart-Schnittstelle , die einen Wert annimmt und Listener benachrichtigt, wenn sie zu einem anderen Wert wechselt. Es ist beispielsweise möglich, einen Ganzzahlzähler in eine einfache Modellklasse zu verpacken:

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

Bei der Arbeit mit komplexen Typen ValueNotifierwird der Operator des darin ==gespeicherten Objekts verwendet, um festzustellen, ob sich der Wert geändert hat.
Erstellen wir das einfachste Provider, das unser Hauptmodell enthält, und es folgt eine Consumerverschachtelte ValueListenableProviderListening-Eigenschaft counter:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

Bitte beachten Sie, dass dieser verschachtelte Anbieter vom Typ ist int. Es kann andere geben. Wenn Sie mehrere Anbieter desselben Typs registriert haben, gibt der Anbieter den "nächsten" (nächsten Vorfahren) zurück.

So hören Sie eine Eigenschaft countervon einem untergeordneten Widget aus:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

Hier erfahren Sie, wie Sie eine Eigenschaft countervon einem anderen Widget aus aktualisieren . Bitte beachten Sie: Wir benötigen Zugriff auf die Originalkopie MyModel.

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

Wenn Sie viele Widgets verwendenProvider, erhalten Sie im Stammverzeichnis der Anwendung eine hässliche Struktur aus vielen Anhängen:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProviderErmöglicht es Ihnen, sie alle auf derselben Ebene zu deklarieren. Es ist nur syntaktischer Zucker: Auf systeminterner Ebene bleiben sie sowieso alle verschachtelt.

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider ist eine interessante Klasse, die in der dritten Paketversion hinzugefügt wurdeProvider. Sie können Anbieter deklarieren, die selbst von anderen Anbietern abhängig sein können, bis zu sechs von einem. In diesem Beispiel ist die Bar-Klasse instanzspezifischFoo. Dies ist nützlich, wenn Sie einen Stammsatz von Diensten kompilieren, die selbst voneinander abhängig sind.

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

Das erste generische Typargument ist der Typ, von dem Ihr Typ abhängt ProxyProvider, und das zweite ist der Typ, den es zurückgibt.

Wie man viele Anbieter gleichzeitig hört


Was ist, wenn ein einzelnes Widget viele Anbieter abhören und neu erstellen soll, wenn sich einer von ihnen ändert? Mit den Widget-Optionen können Sie bis zu 6 Anbieter gleichzeitig anhören Consumer. Wir erhalten Instanzen als zusätzliche Methodenparameter builder.

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

Fazit


Bei Verwendung InheritedWidget Providerkönnen Sie den Status wie in Flutter üblich verwalten. Widgets können auf Statusobjekte zugreifen und diese so abhören, dass der zugrunde liegende Benachrichtigungsmechanismus abstrahiert wird. Es ist einfacher, die Lebensdauer von Statusobjekten zu verwalten, indem Sie Ankerpunkte erstellen, um diese Objekte nach Bedarf zu erstellen und sie bei Bedarf zu entfernen. Dieser Mechanismus kann verwendet werden, um Abhängigkeiten einfach zu implementieren und sogar als Grundlage für erweiterte Statusverwaltungsoptionen. Mit dem Segen von Google und der wachsenden Unterstützung in der Flutter-Community ist es Providerzu einem Paket geworden , das es wert ist, unverzüglich ausprobiert zu werden!

All Articles