Au milieu de l'année dernière, je voulais porter une application Android sur iOS et sur le Web. Flutter était le choix des plates-formes mobiles et je réfléchissais à ce qu'il fallait choisir pour le côté Web.
Bien que je sois tombé amoureux de Flutter à première vue, j’avais encore des réserves: lors de la propagation de l’état dans l’arborescence des widgets, Flutter’s InheritedWidget
ou Redux - avec toutes ses variantes - fera le travail, mais avec un nouveau framework comme Flutter, vous vous attendriez à ce que la couche de vue soit un peu plus réactive, c'est-à-dire que les widgets seraient eux-mêmes sans état et changeraient en fonction de l'état dans lequel ils sont nourris de l'extérieur, mais ils ne le sont pas. De plus, Flutter ne prend en charge qu'Android et iOS, mais je voulais publier sur le Web. J'ai déjà beaucoup de logique métier dans mon application et je voulais la réutiliser autant que possible, et l'idée de changer le code à au moins deux endroits pour un seul changement de logique métier était inacceptable.
J'ai commencé à chercher comment surmonter cela et je suis tombé sur BLoC. Pour une introduction rapide, je recommande de regarder Flutter / AngularDart - Partage de code, mieux ensemble (DartConf 2018) quand tu as le temps.
BLoC est un mot sophistiqué inventé par Google qui signifie ' b usiness la gic c omposants. » L'idée du modèle BLoC est de stocker autant de votre logique métier que possible dans du code Dart pur afin qu'il puisse être réutilisé par d'autres plates-formes. Pour y parvenir, il existe des règles à suivre:
apprendre c vs c++
Une dernière chose à garder à l'esprit est que l'entrée pour un BLoC doit être un évier , tandis que la sortie se fait via un courant . Ces deux éléments font partie de la StreamController
.
Si vous respectez strictement ces règles lors de l'écriture de votre application Web (ou mobile!), La création d'une version mobile (ou Web!) Peut être aussi simple que de créer les vues et les interfaces spécifiques à la plate-forme. Même si vous venez de commencer à utiliser AngularDart ou Flutter, il est toujours facile de créer des vues avec des connaissances de base sur les plates-formes. Vous pouvez finir par réutiliser plus de la moitié de votre base de code. Le motif BLoC garde tout structuré et facile à entretenir.
J'ai fait un simple toute l'application dans Flutter et AngularDart. L'application utilise Firecloud comme back-end et une approche réactive pour la création de vues. L'application comprend trois parties:
bloc
todo_app_flutter
todoapp_dart_angular
Vous pouvez choisir d'avoir plus de parties - par exemple, une interface de données, une interface de localisation, etc. Il ne faut pas oublier que chaque couche doit communiquer avec l'autre via une interface.
Dans le bloc/
annuaire:
lib/src/bloc
: Les modules BloC sont stockés ici sous forme de bibliothèques Dart pures contenant la logique métier.lib/src/repository
: Les interfaces vers les données sont stockées dans le répertoire.lib/src/repository/firestore
: Le référentiel contient l'interface FireCloud aux données avec son modèle, et comme il s'agit d'un exemple d'application, nous n'avons qu'un seul modèle de données todo.dart
et une interface vers les données todo_repository.dart
; cependant, dans une application du monde réel, il y aura plus de modèles et d'interfaces de référentiel.lib/src/repository/preferences
contient preferences_interface.dart
, une interface simple qui stocke les noms d'utilisateurs connectés avec succès dans le stockage local sur le Web ou les préférences partagées sur les appareils mobiles.//BLOC abstract class PreferencesInterface{ //Preferences final DEFAULT_USERNAME = 'DEFAULT_USERNAME'; Future initPreferences(); String get defaultUsername; void setDefaultUsername(String username); }
Les implémentations Web et mobiles doivent implémenter cela dans le magasin et obtenir le nom d'utilisateur par défaut du stockage / des préférences locales. L'implémentation AngularDart de ceci ressemble à ceci:
// ANGULAR DART class PreferencesInterfaceImpl extends PreferencesInterface { SharedPreferences _prefs; @override Future initPreferences() async => _prefs = await SharedPreferences.getInstance(); @override void setDefaultUsername(String username) => _prefs.setString(DEFAULT_USERNAME, username); @override String get defaultUsername => _prefs.getString(DEFAULT_USERNAME); }
Rien de spectaculaire ici - il met en œuvre ce dont il a besoin. Vous remarquerez peut-être le initPreferences()
méthode asynchrone qui renvoie null
. Cette méthode doit être implémentée du côté Flutter depuis l'obtention de SharedPreferences
l'instance sur mobile est asynchrone.
//FLUTTER @override Future initPreferences() async => _prefs = await SharedPreferences.getInstance();
Tenons-nous un peu au répertoire lib / src / bloc. Toute vue qui gère une logique métier doit avoir son composant BLoC. Dans ce répertoire, vous verrez afficher les BLoCs base_bloc.dart
, endpoints.dart
et session.dart
. Le dernier est chargé de connecter et de déconnecter l'utilisateur et de fournir des points de terminaison pour les interfaces du référentiel. La raison pour laquelle l'interface de session existe est que firebase
et firecloud
les packages ne sont pas les mêmes pour le Web et le mobile et doivent être implémentés en fonction de la plate-forme.
// BLOC abstract class Session implements Endpoints { //Collections. @protected final String userCollectionName = 'users'; @protected final String todoCollectionName = 'todos'; String userId; Session(){ _isSignedIn.stream.listen((signedIn) { if(!signedIn) _logout(); }); } final BehaviorSubject _isSignedIn = BehaviorSubject(); Stream get isSignedIn => _isSignedIn.stream; Sink get signedIn => _isSignedIn.sink; Future signIn(String username, String password); @protected void logout(); void _logout() { logout(); userId = null; } }
L'idée est de garder la classe de session globale (singleton). Basé sur son _isSignedIn.stream
getter, il gère le basculement de l'application entre la vue login / todo-list et fournit des points de terminaison aux implémentations du référentiel si l'ID utilisateur existe (c'est-à-dire que l'utilisateur est connecté).
base_bloc.dart
est la base de tous les BLoC. Dans cet exemple, il gère l'indicateur de charge et l'affichage de la boîte de dialogue d'erreur selon les besoins.
Pour l'exemple de logique métier, nous allons examiner todo_add_edit_bloc.dart
. Le nom long du fichier explique son objectif. Il a une méthode void privée _addUpdateTodo(bool addUpdate)
.
// BLOC void _addUpdateTodo(bool addUpdate) { if(!addUpdate) return; //Check required. if(_title.value.isEmpty) _todoError.sink.add(0); else if(_description.value.isEmpty) _todoError.sink.add(1); else _todoError.sink.add(-1); if(_todoError.value >= 0) return; final TodoBloc todoBloc = _todo.value == null ? TodoBloc('', false, DateTime.now(), null, null, null) : _todo.value; todoBloc.title = _title.value; todoBloc.description = _description.value; showProgress.add(true); _toDoRepository.addUpdateToDo(todoBloc) .doOnDone( () => showProgress.add(false) ) .listen((_) => _closeDetail.add(true) , onError: (err) => error.add( err.toString()) ); }
L'entrée de cette méthode est bool addUpdate
et c'est un auditeur de final BehaviorSubject _addUpdate = BehaviorSubject()
. Lorsqu'un utilisateur clique sur le bouton Enregistrer dans l'application, l'événement envoie à ce sujet la valeur réelle du puits et déclenche cette fonction BLoC. Ce morceau de code flottant fait la magie du côté de la vue.
// FLUTTER IconButton(icon: Icon(Icons.done), onPressed: () => _todoAddEditBloc.addUpdateSink.add(true),),
_addUpdateTodo
vérifie que le titre et la description ne sont pas vides et modifie la valeur de _todoError
BehaviorSubject basé sur cette condition. Le _todoError
error est responsable du déclenchement de l'affichage d'erreur de vue sur les champs d'entrée si aucune valeur n'est fournie. Si tout va bien, il vérifie s'il faut créer ou mettre à jour le TodoBloc
et enfin _toDoRepository
fait l'écriture sur FireCloud.
La logique métier est là mais remarquez:
_addUpdateTodo
est privé et n'est pas accessible depuis la vue._title.value
et _description.value
sont remplis par l'utilisateur qui saisit la valeur dans la saisie de texte. La saisie de texte sur l'événement de changement de texte envoie sa valeur aux récepteurs respectifs. De cette façon, nous avons un changement réactif des valeurs dans le BLoC et leur affichage dans la vue._toDoRepository
dépend de la plate-forme et est fourni par injection.Consultez le code du todo_list.dart
BLoC _getTodos()
méthode. Il écoute un instantané de la collection todo et diffuse les données de la collection à répertorier dans sa vue. La liste de vues est redessinée en fonction du changement de flux de collection.
comment utiliser le cadre à ressort
// BLOC void _getTodos(){ showProgress.add(true); _toDoRepository.getToDos() .listen((todosList) { todosSink.add(todosList); showProgress.add(false); }, onError: (err) { showProgress.add(false); error.add(err.toString()); }); }
La chose importante à savoir lors de l'utilisation de flux ou d'un équivalent rx est que les flux doivent être fermés. Nous faisons cela dans le dispose()
méthode de chaque BLoC. Éliminez le BLoC de chaque vue dans sa méthode d'élimination / destruction.
// FLUTTER @override void dispose() { widget.baseBloc.dispose(); super.dispose(); }
Ou dans un projet AngularDart:
// ANGULAR DART @override void ngOnDestroy() { todoListBloc.dispose(); }
Nous avons dit précédemment que tout ce qui entre dans un BLoC doit être simple Dart et rien dépendant de la plate-forme. TodoAddEditBloc
besoins ToDoRepository
écrire à Firestore. Firebase a des packages dépendant de la plate-forme, et nous devons avoir des implémentations séparées de ToDoRepository
interface. Ces implémentations sont injectées dans les applications. Pour Flutter, j'ai utilisé le flutter_simple_dependency_injection
package et il ressemble à ceci:
// FLUTTER class Injection { static Firestore _firestore = Firestore.instance; static FirebaseAuth _auth = FirebaseAuth.instance; static PreferencesInterface _preferencesInterface = PreferencesInterfaceImpl(); static Injector injector; static Future initInjection() async { await _preferencesInterface.initPreferences(); injector = Injector.getInjector(); //Session injector.map((i) => SessionImpl(_auth, _firestore), isSingleton: true); //Repository injector.map((i) => ToDoRepositoryImpl(injector.get()), isSingleton: false); //Bloc injector.map((i) => LoginBloc(_preferencesInterface, injector.get()), isSingleton: false); injector.map((i) => TodoListBloc(injector.get(), injector.get()), isSingleton: false); injector.map((i) => TodoAddEditBloc(injector.get()), isSingleton: false); } }
Utilisez ceci dans un widget comme celui-ci:
meilleures pratiques de conception de sites Web mobiles
// FLUTTER TodoAddEditBloc _todoAddEditBloc = Injection.injector.get();
AngularDart a une injection intégrée via des fournisseurs.
// ANGULAR DART @GenerateInjector([ ClassProvider(PreferencesInterface, useClass: PreferencesInterfaceImpl), ClassProvider(Session, useClass: SessionImpl), ExistingProvider(Endpoints, Session) ])
Et dans un composant:
// ANGULAR DART providers: [ overlayBindings, ClassProvider(ToDoRepository, useClass: ToDoRepositoryImpl), ClassProvider(TodoAddEditBloc), ExistingProvider(BaseBloc, TodoAddEditBloc) ],
Nous pouvons voir que Session
est mondial. Il fournit la fonctionnalité de connexion / déconnexion et les points de terminaison utilisés dans ToDoRepository
et BLoC. ToDoRepository
a besoin d'une interface d'extrémité implémentée dans SessionImpl
etc. La vue ne devrait voir que son BLoC et rien de plus.
Les vues doivent être aussi simples que possible. Ils n’affichent que ce qui provient du BLoC et envoient les données de l’utilisateur au BLoC. Nous reviendrons dessus avec TodoAddEdit
widget de Flutter et son équivalent Web TodoDetailComponent
. Ils affichent le titre et la description de la tâche sélectionnée et l'utilisateur peut ajouter ou mettre à jour une tâche.
Battement:
// FLUTTER _todoAddEditBloc.todoStream.first.then((todo) { _titleController.text = todo.title; _descriptionController.text = todo.description; });
Et plus tard dans le code…
// FLUTTER StreamBuilder( stream: _todoAddEditBloc.todoErrorStream, builder: (BuildContext context, AsyncSnapshot errorSnapshot) { return TextField( onChanged: (text) => _todoAddEditBloc.titleSink.add(text), decoration: InputDecoration(hintText: Localization.of(context).title, labelText: Localization.of(context).title, errorText: errorSnapshot.data == 0 ? Localization.of(context).titleEmpty : null), controller: _titleController, ); }, ),
Le StreamBuilder
Le widget se reconstruit s'il y a une erreur (rien n'est inséré). Cela se produit en écoutant _todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink
, qui est un récepteur dans le BLoC qui contient le titre et est mis à jour lorsque l'utilisateur saisit du texte dans le champ de texte.
La valeur initiale de ce champ de saisie (si un todo est sélectionné) est renseignée en écoutant _todoAddEditBloc.todoStream
qui contient le todo sélectionné ou un vide si nous ajoutons un nouveau todo.
L'attribution d'une valeur à un champ de texte est effectuée par son contrôleur _titleController.text = todo.title;
.
Lorsque l'utilisateur décide d'enregistrer le todo, il appuie sur l'icône de coche dans la barre d'application et il déclenche _todoAddEditBloc.addUpdateSink.add(true)
. Cela invoque le _addUpdateTodo(bool addUpdate)
nous en avons parlé dans la section BLoC précédente et fait toute la logique métier d'ajout, de mise à jour ou d'afficher l'erreur à l'utilisateur.
Tout est réactif et il n'est pas nécessaire de gérer l'état du widget.
comment créer un robot
Le code AngularDart est encore plus simple. Après avoir fourni au composant son BLoC, en utilisant des fournisseurs, le todo_detail.html
Le code de fichier fait la partie d'affichage des données et de renvoi de l'interaction de l'utilisateur au BLoC.
// AngularDart {{saveStr}}
Similaire à Flutter, nous assignons ngModel=
la valeur du flux de titre, qui est sa valeur initiale.
// AngularDart (inputKeyPress)='todoAddEditBloc.descriptionSink.add($event)'
Le inputKeyPress
L'événement de sortie renvoie les caractères saisis par l'utilisateur dans l'entrée de texte à la description du BLoC. Le bouton de matériau (trigger)='todoAddEditBloc.addUpdateSink.add(true)'
événement envoie l'événement BLoC add / update qui déclenche à nouveau le même _addUpdateTodo(bool addUpdate)
fonction dans le BLoC. Si vous jetez un oeil à la todo_detail.dart
code du composant, vous verrez qu'il n'y a presque rien à part les chaînes qui sont affichées sur la vue. Je les ai placés là et non dans le HTML à cause d'une possible localisation qui peut être faite ici.
Il en va de même pour tous les autres composants: les composants et les widgets n'ont aucune logique métier.
Un autre scénario mérite d'être mentionné. Imaginez que vous ayez une vue avec une logique de présentation de données complexe ou quelque chose comme un tableau avec des valeurs qui doivent être formatées (dates, devises, etc.). Quelqu'un pourrait être tenté d'obtenir les valeurs de BLoC et de les formater dans une vue. C'est faux! Les valeurs affichées dans la vue doivent arriver à la vue déjà formatée (chaînes). La raison en est que le formatage lui-même est également une logique métier. Un autre exemple est celui où le formatage de la valeur d'affichage dépend d'un paramètre d'application qui peut être modifié au moment de l'exécution. En fournissant ce paramètre à BLoC et en utilisant une approche réactive pour afficher l'affichage, la logique métier formate la valeur et ne redessine que les parties nécessaires. Le modèle BLoC que nous avons dans cet exemple, TodoBloc
, est très simple. La conversion d'un modèle FireCloud vers le modèle BLoC est effectuée dans le référentiel, mais si nécessaire, elle peut être effectuée dans BLoC afin que les valeurs du modèle soient prêtes à être affichées.
Ce bref article couvre les principaux concepts d'implémentation du modèle BLoC. C'est la preuve que le partage de code entre Flutter et AngularDart est possible, ce qui permet une utilisation native et développement multiplateforme .
En explorant l'exemple, vous verrez que, lorsqu'il est mis en œuvre correctement, BLoC réduit considérablement le temps de création d'applications mobiles / Web. Un exemple est ToDoRepository
et sa mise en œuvre. Le code d'implémentation est presque identique et même la logique de composition de vue est similaire. Après quelques widgets / composants, vous pouvez rapidement démarrer la production de masse.
J'espère que cet article vous donnera un aperçu du plaisir et de l'enthousiasme que j'ai à créer des applications Web / mobiles en utilisant Flutter / AngularDart et le modèle BLoC. Si vous souhaitez créer des applications de bureau multiplateformes en JavaScript, lisez Electron: applications de bureau multiplateformes simplifiées par son collègue ApeeScapeer Stéphane P. Péricat.
En relation: Le langage Dart: quand Java et C # ne sont pas assez précisFlutter est une plateforme de développement mobile (Android / iOS). Il se concentre sur une expérience utilisateur native de haute qualité et sur le développement rapide d'applications d'interface utilisateur riches.
Flutter utilise Dart, un langage moderne, concis et orienté objet développé par Google.
AngularDart est un portage de Angular vers Dart. Son code Dart est compilé en JavaScript.
Le compilateur prend en charge IE11, Chrome, Edge, Firefox et Safari.
«Business Logic Component» ou BLoC est un modèle de développement. L'idée autour de BLoC est d'isoler autant de logique métier que possible dans du code Dart pur. Cela donne une base de code qui peut être partagée entre les plates-formes mobiles et Web.
comment faire une vidéo explicative dans after effects
Les applications conçues pour BLoC doivent être superposées. La couche de vue, la couche de logique métier, la couche d'accès aux données, etc. doivent être séparées. Chaque couche communique avec la couche ci-dessous via des interfaces. L'interface doit être du pur code Dart et l'implémentation de l'interface peut être spécifique à la plate-forme (Flutter ou AngularDart).
Il y en a quatre: les entrées et les sorties d'un BLoC ne doivent être que des flux et des récepteurs, les dépendances doivent être injectables et indépendantes de la plate-forme, la logique métier ne doit pas avoir de branchement de plate-forme et vous pouvez gérer l'état d'affichage ou utiliser une approche réactive.
Le modèle BLoC ne se soucie pas de la vue et de la façon dont il gère l'affichage / l'interaction de l'utilisateur. Mais, comme il n'utilise que des flux et des puits comme sorties et entrées, il est parfaitement adapté pour une approche réactive du côté de la vue.
Flutter diffère de la plupart des autres solutions en ce qu'il utilise son propre moteur de rendu haute performance pour dessiner des widgets.