
(ursprünglich auf Medium veröffentlicht )
Flutter bietet ein modernes Framework im Reaktionsstil, eine umfangreiche Widget-Sammlung und Tools, aber es gibt nichts Vergleichbares zu Android's Handbuch zur App-Architektur .
In der Tat gibt es keine ultimative Architektur, die alle möglichen Anforderungen erfüllt. Stellen wir uns jedoch der Tatsache, dass die meisten mobilen Apps, an denen wir arbeiten, mindestens einige der folgenden Funktionen aufweisen:
- Daten vom / zum Netzwerk anfordern / hochladen.
- Ordnen Sie Daten zu, transformieren Sie sie, bereiten Sie sie vor und präsentieren Sie sie dem Benutzer.
- Daten in / aus der Datenbank ablegen / abrufen.
In Anbetracht dessen habe ich eine Beispiel-App erstellt, die genau das gleiche Problem mit drei verschiedenen Ansätzen für die Architektur löst.
Dem Benutzer wird in der Mitte des Bildschirms die Schaltfläche „Benutzerdaten laden“ angezeigt. Wenn der Benutzer auf die Schaltfläche klickt, wird das Laden asynchroner Daten ausgelöst und die Schaltfläche durch eine Ladeanzeige ersetzt. Nach dem Laden der Daten wird die Ladeanzeige durch die Daten ersetzt.
Fangen wir an.

Daten
Der Einfachheit halber habe ich eine Repository
Klasse erstellt, die die Methode getUser()
enthält, die einen asynchronen Netzwerkaufruf emuliert und das Future<User>
-Objekt mit fest codierten Werten zurückgibt.
Wenn Sie mit Futures und asynchroner Programmierung in Dart nicht vertraut sind, können Sie mehr darüber erfahren, indem Sie diesem Tutorial folgen und ein Dokument 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 App so erstellen, wie es die meisten Entwickler nach dem Lesen der offiziellen Flutter-Dokumentation tun würden.
Navigieren zum VanillaScreen
Bildschirm mit Navigator
Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), );
Da sich der Status des Widgets während der Lebensdauer des Widgets mehrmals ändern kann, sollten wir StatefulWidget
. Für die Implementierung eines Stateful-Widgets ist außerdem eine State
Klasse erforderlich. Die Felder bool _isLoading
und User _user
in der _VanillaScreenState
Klasse repräsentieren den Status des Widgets. Beide Felder werden initialisiert, bevor die build(BuildContext context)
Methode build(BuildContext context)
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(), ); } }
Wenn das Widget- build(BuildContext context)
erstellt wird build(BuildContext context)
wird die build(BuildContext context)
Methode build(BuildContext context)
aufgerufen, um die Benutzeroberfläche zu erstellen. Alle Entscheidungen über die Widgets, die zur Darstellung des aktuellen Status erstellt werden sollen, werden im UI-Deklarationscode getroffen.
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
Um die Fortschrittsanzeige anzuzeigen, wenn der Benutzer auf die Schaltfläche "Benutzerdetails laden" klickt, gehen wir wie folgt vor.
setState(() { _isLoading = true; });
Durch Aufrufen von setState () wird das Framework benachrichtigt, dass sich der interne Status dieses Objekts so geändert hat, dass sich dies auf die Benutzeroberfläche in diesem Teilbaum auswirken kann. Dadurch plant das Framework einen Build für dieses Statusobjekt.
Das bedeutet, dass nach dem Aufruf der setState()
-Methode die build(BuildContext context)
Methode build(BuildContext context)
erneut vom Framework aufgerufen wird und der gesamte Widget-Baum neu erstellt wird . Da _isLoading
jetzt auf true
gesetzt ist, wird _buildLoading()
anstelle von _buildBody()
und die Ladeanzeige wird auf dem Bildschirm angezeigt. Genau das gleiche passiert, wenn wir den Rückruf von getUser()
und setState()
_user
Felder _isLoading
und _user
neu _isLoading
.
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
Vorteile
- Leicht zu lernen und zu verstehen.
- Es sind keine Bibliotheken von Drittanbietern erforderlich.
Nachteile
- Der gesamte Widget-Baum wird jedes Mal neu erstellt, wenn sich der Widget-Status ändert.
- Es bricht das Prinzip der Einzelverantwortung. 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.
- Entscheidungen darüber, wie der aktuelle Status dargestellt werden soll, werden im UI-Deklarationscode getroffen. Wenn wir einen etwas komplexeren Statuscode hätten, würde die Lesbarkeit abnehmen.
Modell mit Gültigkeitsbereich
Scoped Model ist ein Paket von Drittanbietern, das nicht im Flutter-Framework enthalten ist. So beschreiben es die Entwickler von Scoped Model:
Eine Reihe von Dienstprogrammen, mit denen Sie ein Datenmodell einfach von einem übergeordneten Widget an seine Nachkommen übergeben können. Darüber hinaus werden alle untergeordneten Elemente neu erstellt, die das Modell verwenden, wenn das Modell aktualisiert wird. Diese Bibliothek wurde ursprünglich aus der Fuchsia-Codebasis extrahiert.
Erstellen wir denselben Bildschirm mit Scoped Model. Zuerst müssen wir das Scoped Model-Paket installieren, indem scoped_model
Abhängigkeit pubspec.yaml
zu pubspec.yaml
im Abschnitt Abhängigkeiten pubspec.yaml
.
scoped_model: ^1.0.1
Schauen wir uns das UserModelScreen
Widget an und vergleichen es mit dem vorherigen Beispiel, das ohne Verwendung des Bereichsmodells erstellt wurde. Schauen wir uns das UserModelScreen-Widget an und vergleichen es mit dem vorherigen Beispiel, das ohne Verwendung des Bereichsmodells erstellt wurde. Da wir unser Modell allen Nachkommen des Widgets zur Verfügung stellen möchten, sollten wir es mit generischem ScopedModel umschließen und ein Widget und ein Modell bereitstellen.
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 gesamte Widget-Baum neu erstellt, als sich der Status des Widgets änderte. Aber müssen wir tatsächlich den gesamten Bildschirm neu erstellen? Zum Beispiel sollte sich AppBar überhaupt nicht ändern, sodass es keinen Sinn macht, sie neu zu erstellen. Im Idealfall sollten wir nur die aktualisierten Widgets neu erstellen. Scoped Model kann uns dabei helfen, das zu lösen.
ScopedModelDescendant<UserModel>
Widget ScopedModelDescendant<UserModel>
wird verwendet, um UserModel
in der Widget- UserModel
zu finden. Es wird automatisch neu erstellt, wenn das UserModel
benachrichtigt, dass eine Änderung stattgefunden hat.
Eine weitere Verbesserung besteht darin, dass UserModelScreen
nicht mehr für die UserModelScreen
und die Geschäftslogik verantwortlich ist.
UserModel
wir einen Blick auf den UserModel
Code.
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); }
Jetzt hält UserModel
den UserModel
und verwaltet ihn. Um Listener zu benachrichtigen (und Nachkommen neu zu notifyListeners()
), dass die Änderung stattgefunden hat, sollte die Methode notifyListeners()
aufgerufen werden.
Vorteile
- Geschäftslogik, Statusverwaltung und UI-Codetrennung.
- Leicht zu lernen.
Nachteile
- Benötigt eine Bibliothek eines Drittanbieters.
- Da das Modell immer komplexer wird, ist es schwierig zu verfolgen, wann Sie
notifyListeners()
aufrufen notifyListeners()
.
BLoC
BLoC (Business Logic Components) ist ein von Google-Entwicklern empfohlenes Muster. Es nutzt die Stream-Funktionalität, um Statusänderungen zu verwalten und weiterzugeben.
Für Android-Entwickler: Sie können sich das Bloc
Objekt als ViewModel
und StreamController
als LiveData
. Dies macht den folgenden Code sehr einfach, da Sie bereits mit den Konzepten 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; }
Es sind keine zusätzlichen Methodenaufrufe erforderlich, um Teilnehmer zu benachrichtigen, wenn sich der Status ändert.
Ich habe 3 Klassen erstellt, um mögliche Zustände des Bildschirms 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 geladen werden.
UserDataState
für den Status, in dem Daten geladen und auf dem Bildschirm angezeigt werden.
Durch die Weitergabe von Statusänderungen auf diese Weise können wir die gesamte Logik im UI-Deklarationscode entfernen. Im Beispiel mit Scoped Model haben wir immer noch überprüft, ob _isLoading
im UI-Deklarationscode true
ist, um zu entscheiden, welches Widget _isLoading
soll. Im Fall von BLoC verbreiten wir den Status des Bildschirms und die einzige Verantwortung des UserBlocScreen
Widgets besteht darin, die Benutzeroberfläche für diesen Status zu rendern.
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(); } }
UserBlocScreen
Code wurde im Vergleich zu den vorherigen Beispielen noch einfacher. Um die Änderungen der Statusänderungen zu hören, verwenden wir StreamBuilder . StreamBuilder
ist ein StatefulWidget
, das sich selbst basierend auf dem neuesten Snapshot der Interaktion mit einem Stream erstellt .
Vorteile
Es werden keine Bibliotheken von Drittanbietern benötigt.
Geschäftslogik, Statusverwaltung und UI-Logiktrennung.
Es ist reaktiv. Es sind keine zusätzlichen Aufrufe erforderlich, wie im Fall von notifyListeners()
Scoped Model.
Nachteile
Erfahrung im Umgang mit Streams oder RXDART ist erforderlich.
Links
Sie können den Quellcode der obigen Beispiele aus diesem Github-Repo auschecken.
Ursprünglich wurde der Artikel auf Medium veröffentlicht