Grundlagen der Flatter-Anwendungsarchitektur: Vanilla, Scoped Model, BLoC


(Originalartikel in englischer Sprache auf Medium veröffentlicht )


Flutter bietet ein modernes Responsive Framework, eine große Auswahl an Widgets und Tools. Leider ist die Dokumentation nichts anderes als eine Anleitung zur empfohlenen Architektur der Android-Anwendung .


Es gibt keine ideale, universelle Architektur, die den denkbaren Anforderungen einer technischen Aufgabe gerecht werden könnte, aber lassen Sie uns zugeben, dass die meisten mobilen Anwendungen, an denen wir arbeiten, die folgenden Funktionen haben:


  1. Daten anfordern und herunterladen.
  2. Transformation und Aufbereitung von Daten für den Benutzer.
  3. Schreiben und Lesen von Daten aus einer Datenbank oder einem Dateisystem.

Vor diesem Hintergrund habe ich eine Demo-Anwendung erstellt, die das gleiche Problem mit unterschiedlichen Architekturansätzen löst.


Zunächst wird dem Benutzer ein Bildschirm mit der Schaltfläche „Benutzerdaten laden“ in der Mitte angezeigt. Wenn der Benutzer auf die Schaltfläche klickt, erfolgt ein asynchrones Laden der Daten, und die Schaltfläche wird durch eine Ladeanzeige ersetzt. Wenn der Daten-Download abgeschlossen ist, wird die Download-Anzeige durch Daten ersetzt.


Also fangen wir an.



Daten


Um die Aufgabe zu vereinfachen, habe ich die Repository Klasse erstellt, die die Methode getUser() enthält. Diese Methode simuliert das asynchrone Laden von Daten aus dem Netzwerk und gibt Future<User> .


Wenn Sie mit Futures und asynchroner Programmierung in Dart nicht vertraut sind, können Sie hier mehr darüber lesen und die Dokumentation der Future-Klasse lesen.


 class Repository { Future<User> getUser() async { await Future.delayed(Duration(seconds: 2)); return User(name: 'John', surname: 'Smith'); } } 

 class User { User({ @required this.name, @required this.surname, }); final String name; final String surname; } 

Vanille


Lassen Sie uns die Anwendung entwickeln, wie es ein Entwickler tun würde, wenn er die Flutter-Dokumentation auf der offiziellen Website lesen würde.


Öffnen Sie den VanillaScreen Bildschirm mit Navigator


 Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), ); 

Da sich der Status des Widgets während seines Lebenszyklus mehrmals ändern kann, müssen wir von StatefulWidget erben. Um Ihr Stateful-Widget zu implementieren, benötigen Sie auch die State Klasse. Die Felder bool _isLoading und User _user in der Klasse _VanillaScreenState repräsentieren den Status des Widgets. Beide Felder werden initialisiert, bevor die build(BuildContext context) Methode build(BuildContext context) zum ersten Mal aufgerufen wird.


 class VanillaScreen extends StatefulWidget { VanillaScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _VanillaScreenState(); } class _VanillaScreenState extends State<VanillaScreen> { bool _isLoading = false; User _user; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Vanilla'), ), body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ), ); } Widget _buildBody() { if (_user != null) { return _buildContent(); } else { return _buildInit(); } } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { setState(() { _isLoading = true; }); widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); }, ), ); } Widget _buildContent() { return Center( child: Text('Hello ${_user.name} ${_user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } } 

Nachdem das build(BuildContext context) des Widgets erstellt wurde, wird die build(BuildContext context) Methode build(BuildContext context) aufgerufen, um die Benutzeroberfläche zu build(BuildContext context) . Alle Entscheidungen darüber, welches Widget auf dem Bildschirm angezeigt werden soll, werden direkt im Deklarationscode der Benutzeroberfläche getroffen.


 body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ) 

Um eine Fortschrittsanzeige anzuzeigen, gehen Sie wie folgt vor, wenn der Benutzer auf die Schaltfläche „Benutzerdetails laden“ klickt.


 setState(() { _isLoading = true; }); 

Aus der Dokumentation (Übersetzung):


Ein Aufruf der Methode setState () benachrichtigt das Framework, dass sich der interne Status dieses Objekts geändert hat und sich auf die Benutzeroberfläche im Teilbaum auswirken kann. Aus diesem Grund ruft das Framework die Erstellungsmethode für dieses Statusobjekt auf.

Dies bedeutet, dass das setState() nach dem Aufrufen der setState() -Methode die build(BuildContext context) Methode build(BuildContext context) erneut build(BuildContext context) , wodurch der gesamte Widget-Baum neu erstellt wird . Da sich der Wert des _isLoading in true geändert _isLoading , wird anstelle der Methode _buildLoading() Methode _buildLoading() aufgerufen und eine Fortschrittsanzeige auf dem Bildschirm angezeigt.
Genau das Gleiche passiert, wenn wir einen Rückruf von getUser() und die Methode aufrufen
setState() , um den Feldern _isLoading und _user neue Werte _user .


 widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); 

Vorteile


  1. Niedrige Eintrittsschwelle.
  2. Keine Bibliotheken von Drittanbietern erforderlich.

Nachteile


  1. Wenn sich der Status des Widgets ändert, wird der Widget-Baum jedes Mal vollständig neu erstellt.
  2. Verstößt gegen den Grundsatz der alleinigen Verantwortung. Das Widget ist nicht nur für die Erstellung der Benutzeroberfläche verantwortlich, sondern auch für das Laden von Daten, die Geschäftslogik und die Statusverwaltung.
  3. Entscheidungen zur Anzeige des aktuellen Status werden direkt im UI-Code getroffen. Wenn der Zustand komplexer wird, nimmt die Lesbarkeit des Codes stark ab.

Modell mit Gültigkeitsbereich


Scoped Model ist eine Bibliothek eines Drittanbieters . So beschreiben es die Entwickler:


Eine Reihe von Dienstprogrammen, mit denen Sie das Datenmodell des Ahnen-Widgets auf alle Nachkommen übertragen können. Wenn sich die Modelldaten ändern, werden außerdem alle Nachkommen, die das Modell verwenden, neu erstellt. Diese Bibliothek wurde ursprünglich aus dem Fuchsia- Projektcode entnommen.

Lassen Sie uns den gleichen Bildschirm wie im vorherigen Beispiel erstellen, jedoch das Scoped-Modell verwenden. Zuerst müssen wir dem Projekt die Scoped Model-Bibliothek hinzufügen. Fügen Sie die Abhängigkeit scoped_model Datei scoped_model im Abschnitt dependencies .


 scoped_model: ^1.0.1 

Schauen wir uns den UserModelScreen Code an und vergleichen ihn mit dem vorherigen Beispiel, in dem wir das Scoped-Modell nicht verwendet haben. Um unser Modell für die Nachkommen des Widgets zugänglich zu machen, müssen wir das Widget und das Modell in ScopedModel .


 class UserModelScreen extends StatefulWidget { UserModelScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserModelScreenState(); } class _UserModelScreenState extends State<UserModelScreen> { UserModel _userModel; @override void initState() { _userModel = UserModel(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return ScopedModel( model: _userModel, child: Scaffold( appBar: AppBar( title: const Text('Scoped model'), ), body: SafeArea( child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) { return _buildLoading(); } else { if (model.user != null) { return _buildContent(model); } else { return _buildInit(model); } } }, ), ), ), ); } Widget _buildInit(UserModel userModel) { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { userModel.loadUserData(); }, ), ); } Widget _buildContent(UserModel userModel) { return Center( child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } } 

Im vorherigen Beispiel wurde der Widget-Baum jedes Mal, wenn ein Widget den Status ändert, vollständig neu erstellt. Aber müssen wir wirklich den gesamten Widget-Baum (Vollbild) neu erstellen? Beispielsweise ändert sich die AppBar überhaupt nicht, und es macht keinen Sinn, sie neu zu erstellen. Im Idealfall sollten Sie nur die Widgets neu erstellen, die sich entsprechend der Statusänderung ändern sollten. Das Scoped-Modell kann uns bei der Lösung dieses Problems helfen.


Das ScopedModelDescendant<UserModel> Widget ScopedModelDescendant<UserModel> wird verwendet, um das UserModel in der Widget- UserModel zu finden. Es wird jedes Mal automatisch neu erstellt, wenn das UserModel eine Änderung benachrichtigt.


Eine weitere Verbesserung besteht darin, dass UserModelScreen nicht mehr für die UserModelScreen , die Geschäftslogik und das Laden von Daten verantwortlich ist.


Schauen wir uns den Code für die UserModel Klasse an.


 class UserModel extends Model { UserModel(this._repository); final Repository _repository; bool _isLoading = false; User _user; User get user => _user; bool get isLoading => _isLoading; void loadUserData() { _isLoading = true; notifyListeners(); _repository.getUser().then((user) { _user = user; _isLoading = false; notifyListeners(); }); } static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); } 

UserModel enthält und verwaltet jetzt den Status. Um Listener zu benachrichtigen (und Nachkommen neu zu erstellen), dass eine Änderung aufgetreten ist, müssen Sie die Methode notifyListeners() .


Vorteile


  1. Statusverwaltung, Geschäftslogik und Laden von Daten sind vom UI-Code getrennt.
  2. Niedrige Eintrittsschwelle.

Nachteile


  1. Bibliotheksabhängigkeit von Drittanbietern.
  2. Wenn das Modell komplex genug wird, ist es schwierig zu verfolgen, wann es wirklich notwendig ist, die notifyListeners() -Methode notifyListeners() , um unnötige Neuerstellungen zu vermeiden.

BLoC


BLoC (Business Logic Components) ist ein Muster, das von Entwicklern von Google empfohlen wird. Streams werden zum Verwalten des Status und zum Benachrichtigen über eine Statusänderung verwendet.


Für Android-Entwickler: Sie können sich vorstellen, dass Bloc ein ViewModel und StreamController ein LiveData . Dadurch wird der folgende Code leicht verständlich, da Sie bereits mit den Grundprinzipien vertraut sind.


 class UserBloc { UserBloc(this._repository); final Repository _repository; final _userStreamController = StreamController<UserState>(); Stream<UserState> get user => _userStreamController.stream; void loadUserData() { _userStreamController.sink.add(UserState._userLoading()); _repository.getUser().then((user) { _userStreamController.sink.add(UserState._userData(user)); }); } void dispose() { _userStreamController.close(); } } class UserState { UserState(); factory UserState._userData(User user) = UserDataState; factory UserState._userLoading() = UserLoadingState; } class UserInitState extends UserState {} class UserLoadingState extends UserState {} class UserDataState extends UserState { UserDataState(this.user); final User user; } 

Der Code zeigt, dass keine zusätzlichen Methoden mehr aufgerufen werden müssen, um über Statusänderungen zu benachrichtigen.


Ich habe 3 Klassen erstellt, um mögliche Zustände darzustellen:


UserInitState für den Status, in dem der Benutzer einen Bildschirm mit einer Schaltfläche in der Mitte öffnet.


UserLoadingState für den Status, in dem die Ladeanzeige angezeigt wird, während Daten UserLoadingState .


UserDataState für den Status, in dem Daten bereits geladen und auf dem Bildschirm angezeigt werden.


Wenn wir den Status auf diese Weise übergeben, können wir die Logik im UI-Code vollständig entfernen. Im Beispiel für das Bereichsmodell haben wir weiterhin überprüft, ob der Wert des _isLoading true oder false , um zu bestimmen, welches Widget erstellt werden soll. Im Fall von BLoC übergeben wir den neuen Status an den Stream. Die einzige Aufgabe des UserBlocScreen Widgets UserBlocScreen , eine Benutzeroberfläche für den aktuellen Status UserBlocScreen erstellen.


 class UserBlocScreen extends StatefulWidget { UserBlocScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserBlocScreenState(); } class _UserBlocScreenState extends State<UserBlocScreen> { UserBloc _userBloc; @override void initState() { _userBloc = UserBloc(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc'), ), body: SafeArea( child: StreamBuilder<UserState>( stream: _userBloc.user, initialData: UserInitState(), builder: (context, snapshot) { if (snapshot.data is UserInitState) { return _buildInit(); } if (snapshot.data is UserDataState) { UserDataState state = snapshot.data; return _buildContent(state.user); } if (snapshot.data is UserLoadingState) { return _buildLoading(); } }, ), ), ); } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { _userBloc.loadUserData(); }, ), ); } Widget _buildContent(User user) { return Center( child: Text('Hello ${user.name} ${user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } @override void dispose() { _userBloc.dispose(); super.dispose(); } } 

Der Code für das UserBlocScreen Widget ist noch einfacher als in den vorherigen Beispielen. Um Statusänderungen abzuhören, wird StreamBuilder verwendet. StreamBuilder ist ein StatefulWidget , das sich selbst gemäß dem letzten Wert (Snapshot) des Streams ( Stream ) erstellt.


Vorteile


  1. Keine Bibliotheken von Drittanbietern erforderlich.
  2. Geschäftslogik, Statusverwaltung und Laden von Daten sind vom UI-Code getrennt.
  3. Reaktivität Es müssen keine zusätzlichen Methoden aufgerufen werden, wie im Beispiel mit notifyListeners() .

Nachteile


  1. Die Eintrittsschwelle ist etwas höher. Benötigen Sie Erfahrung mit Streams oder RXDART.

Links


Sie können den vollständigen Code lesen, indem Sie ihn aus meinem Github-Repository herunterladen.


Originalartikel auf Medium veröffentlicht

Source: https://habr.com/ru/post/de438574/


All Articles