Utiliser MobX avec Flutter
Dans ce post, nous allons comprendre comment faire la gestion d’état de l’application Flutter avec le framework Mobx. Rappelons nous que l’état (state) représente l’ensemble des données susceptible d’être modifiées pendant l’exécution de l’application.
Lorsqu’on développe une application, que l’on soit junior, intermédiaire ou senior, à un moment donné on se pose toujours la question de savoir comment allons-nous faire la gestion d’état de l’application. Par exemple, la gestion locale de l’état devient insuffisante lorsqu’un composant doit accéder à ou modifier l’état d’un autre composant.
Pour gérer l’état, le framework Mobx est un choix parmi tant d’autres.
A la base utilisé dans l’écoysystème Javascript, il a été porté en Dart et est aujourd’hui l’une des librairies les plus utilisés sur pub.dev
Contrairement à la plupart des librairies disponible pour la gestion d’état, Mobx s’appuie fortement sur la génération de code, ce qui permet une prise en main plus facile et laisse la place pour mieux se concentrer sur la valeur à ajouter pour notre application. Merci au boilerplate coding.
Pour mieux prendre en main Mobx avec Flutter, je vous propose de dérouler l’apprentissage sur un exemple concret d’application. Nous ferons l’exemple avec une simple application pour nous donner les informations sur le climat.
Nous n’allons pas mettre l’accent sur l’UI. Nous allons simuler des appels HTTP vers une API FakeWeatherAPI
qui génére des climats aléatoires.
Le modèle pour le climat sera Weather
et contiendra la température et la ville associée.
1import 'package:meta/meta.dart';
2
3class Weather {
4 final String cityName;
5 final double temperatureCelsius;
6
7 Weather({
8 @required this.cityName,
9 @required this.temperatureCelsius,
10 });
11}
Ajout des dépendances
L’utilisation de Mobx en Flutter se découpe en 3 librairies :
- une librairie core
- une librairie spécifique pour Flutter
- la librairie dédiée à la génération de code.
Dans notre application en exemple , nous allons aussi utiliser la librairie provider qui va nous permettre de faire de l’injection de dépendance d’une manière un peu plus élégante. Ci-dessous , un extrait des dépendances dans le pubspec.yaml de notre project :
1dependencies:
2 flutter:
3 sdk: flutter
4 # MobX core
5 mobx: ^1.2.1+2
6 # MobX Flutter specific
7 flutter_mobx: ^1.1.0+2
8 # Provider for Dependency Injection
9 provider: ^4.3.2+2
10 # A Flutter package which provides a Search Widget
11 # for selecting an option from a data list.
12 search_widget:
13
14dev_dependencies:
15 flutter_test:
16 sdk: flutter
17 build_runner:
18 # Codegen for MobX code generation
19 mobx_codegen: ^1.1.0+1
A. Fonctionnement de MobX
L’implémentation qui se rapproche le plus du principe de Mobx , est celle du simple ChangeNotifier
implémenté dans le SDK Flutter . On renseigne quelques champs dans une classe nommée Store
. Les valeurs de ces champs
sont en général connus sous le nom d’état
ou state
dans le jargon et sont mutables directement à l’intérieur du store
. Dans ce sens , on peut facilement mettre à jour notre UI quand la valeur d’un champ change en l’observant.
Effectivement, les stores de MobX sont bien plus évolués que le ChangeNotifier
mais le principe
reste le même, contrairement au pattern BLoC ou Redux qui émettent des nouveaux états et non des mutations.
L’implémentation avec le ChangeNotifier
peut laisser place à un plat de spaghetti, alors que Mobx veut apporter une certaine structure.
- Les champs qui sont mutables sont marqués
@observable
- Il est possible d’implémenter de la logique dans le store pour qu’un champ observé change de valeur. Et sauvegarder le résultat dans une propriété marquée
@computed
- L’interface utilisateur peut déclencher ( trigger) une logique implémentée et changer l’état de l’application. Cependant, pour pouvoir modifier l’état , il faut que cette logique soit annotée
@action
Le schéma ci-dessous illustre le workflow avec MobX:
Nous n’avons pas encore parlé de reactions. Ci-dessous le schéma officiel de Mobx et je vous invite aussi à lire la documentation officielle
B. Implémentation dans notre application
B-1. Création du WeatherStore
Le bout de code ci-dessous sert à appuyer la lecture que vous auriez éventuellement faite de la documentation officielle. Créons le store qui va gérer l’état de notre application. Dans un dossier stores, créons le fichier weather_store.dart
1import 'package:mobx/mobx.dart';
2
3part 'weather_store.g.dart';
4
5// Stores
6// -------
7
8class WeatherStore = _WeatherStore with _$WeatherStore;
9
10abstract class _WeatherStore with Store {
11
12}
C’est à peu comme ça le boilerplate avec MobX. Là, tu dois te demander, en Dart
on traite une classe comme une variable ? La ligne 8 équivaut à la ligne ci-dessous, il s’agit de la syntaxe courte:
class WeatherStore extends _WeatherStore with _$WeatherStore {}
_$WeatherStore
est un mixin qui sera généré lors du processus de génération de code avec la librairie nommée mobx_codegen.
Pour déclencher la génération de code , il suffit de lancer la commande ci-dessous :
flutter packages pub run build_runner watch
Nous allons faire évoluer le store pour l’injecter la classe qui va simuler notre API. Par ce scénario, nous allons le rajouter dans le constructeur de notre store comme montré ci-dessous :
1class WeatherStore extends _WeatherStore with _$WeatherStore {
2 WeatherStore(WeatherRepository weatherRepository) : super(weatherRepository);
3}
4
5abstract class _WeatherStore with Store {
6 final WeatherRepository _weatherRepository;
7
8 _WeatherStore(this._weatherRepository);
9}
B-2. Les champs observable
Evidemment les champs annotés @observable
sont fait pour être observés 😀 . L’observation peut se faire de n’importe quel endroit, dans l’UI, la réaction et même dans le store.
On voudra observer l’instance de l’objet lié au climat Weather
pour mettre à jour l’UI ou renvoyer un message si une erreur se produit.
1abstract class _WeatherStore with Store {
2 final WeatherRepository _weatherRepository;
3
4 _WeatherStore(this._weatherRepository);
5
6 @observable
7 Weather weather;
8
9 @observable
10 String errorMessage;
11}
Pour une meilleure gestion du cycle de vie de notre application, nous allons utilisé des objets asynchrones Future
, pour par exemple afficher une barre de progression lorsque l’utilisateur est en attente d’un résultat. Pour ça , nous allons utilisé ObservableFuture<Weather>
qui n’est rien d’autre qu’un wrapper de l’objet Future
mais qui permet d’observer les états pending, fulfilled ou rejected
1...
2@observable
3ObservableFuture<Weather> _weatherFuture;
4
5@observable
6Weather weather;
7
8@observable
9String errorMessage;
10...
Je fais le choix de rajouter un enum
pour connaitre les états transitoires :
1enum StoreState { initial, loading, loaded }
B-3. Les champs computed
On utilise l’annotation @computed
pour définir un type spécial de propriété observable qui sera mise à jour
chaque fois qu’un champ observable changera de valeur.
Ce scénario est parfait pour l’énumération StoreState
. Il devrait changer chaque fois que le statut de l’observable _weatherFuture
change.
1@observable
2ObservableFuture<Weather> _weatherFuture;
3...
4@computed
5StoreState get state {
6 // If the user has not yet searched for a weather forecast or there has been an error
7 if (_weatherFuture == null ||
8 _weatherFuture.status == FutureStatus.rejected) {
9 return StoreState.initial;
10 }
11 // Pending Future means "loading"
12 // Fulfilled Future means "loaded"
13 return _weatherFuture.status == FutureStatus.pending
14 ? StoreState.loading
15 : StoreState.loaded;
16}
On pourrait se poser la question de savoir, quand est ce que la propriété annotée
@computed
sait qu’il faut se mettre à jour. C’est la magique de MobX ! Il ya un code généré qui nous masque cette complexité.
B-4. Les méthodes action
Si vous êtes familé avec l’API ChangeNotifier
, vous savez qu’il faut appeler la méthode notifyListeners()
pour notifier d’un changement d’état. Avec MobX, c’est la même approche, mais au lieu de le déclencher nous même, MobX le fait pour nous lorsqu’une méthode est annoté @action
La méthode getWeather
est une action qui est déclenchée sur changement d’état.
1abstract class _WeatherStore with Store {
2 ...
3
4 @action
5 Future getWeather(String cityName) async {
6 try {
7 // Reset the possible previous error message.
8 errorMessage = null;
9 // Fetch weather from the repository and wrap the regular Future into an observable.
10 // This _weatherFuture triggers updates to the computed state property.
11 _weatherFuture =
12 ObservableFuture(_weatherRepository.fetchWeather(cityName));
13 // ObservableFuture extends Future - it can be awaited and exceptions will propagate as usual.
14 weather = await _weatherFuture;
15 } on NetworkError {
16 errorMessage = "Couldn't fetch weather. Is the device online?";
17 }
18 }
C. Manipulation de l’UI
En utilisant MobX, l’UI travaille principalement avec 2 objets :
- un widget de type
Observer
pour pouvoir reconstruire l’arbre des widgets affichés - les réactions pour éxecuter de la logique UI comme par exemple affiché un objet de type
SnackBar
à l’écran.
Grace à la librairie provider, nous allons préparer l’objet nous permettant d’injecter le WeatherStore
dans l’application :
1 ...
2
3class StoresProvider extends StatelessWidget {
4 final Widget child;
5
6 StoresProvider({@required this.child});
7
8 @override
9 Widget build(BuildContext context) {
10 return MultiProvider(
11 providers: [
12 Provider<WeatherStore>(create: (_) => WeatherStore(FakeWeatherAPI())),
13 ],
14 child: child,
15 );
16 }
17}
Par simplicité, dans le
main.dart
de l’application , nous allons définir la classeStoresProvider
comme root des noeuds de widgets de l’application pour résoudre l’injection de dépendance.
1 ...
2
3void main() => runApp(MyApp());
4
5class MyApp extends StatelessWidget {
6 @override
7 Widget build(BuildContext context) {
8 return StoresProvider(
9 child: MaterialApp(
10 title: 'Material App',
11 home:WeatherSearchPage(),
12 ),
13 );
14 }
15}
On manipule le store dans la page WeatherSearchPage
. On recupère une instance grace à l’API provider.
1class _WeatherSearchPageState extends State<WeatherSearchPage> {
2 WeatherStore _weatherStore;
3
4 @override
5 void didChangeDependencies() {
6 super.didChangeDependencies();
7 _weatherStore ??= Provider.of<WeatherStore>(context);
8 }
9
10 ...
11}
C-1. Une réation dans le SnackBar
Les reactions
permettent de déclencher une fonction lorsque un champ observé est mis à jour. Les scénarios les plus fréquents sont les mises à jour de l’UI ( animation , affichage de barre de progression, dialogue d’alerte ). Dans Mobx, il y a plusieurs types de réactions, et dans l’application nous allons utiliser la plus simple nommé juste reaction
Ci-dessous , la logique d’utilisation dans la page weather_search_page
. Dès qu’on récupère l’instance de WeatherStore
dans la méthode didChangeDependencies()
, on crée une reaction
qui renvoie un objet de type ReactionDisposer
qui est sauvegardé dans la liste _disposers
.
La bonne pratique constite à nettoyer cette liste dans le dispose
de la page , ajouter une clé _scaffoldKey
au Scaffold utilisé pour construire notre page.
1class _WeatherSearchPageState extends State<WeatherSearchPage> {
2 WeatherStore _weatherStore;
3 List<ReactionDisposer> _disposers;
4 // For showing a SnackBar
5 GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
6
7 @override
8 void didChangeDependencies() {
9 super.didChangeDependencies();
10 _weatherStore ??= Provider.of<WeatherStore>(context);
11 _disposers ??= [
12 reaction(
13 // Tell the reaction which observable to observe
14 (_) => _weatherStore.errorMessage,
15 // Run some logic with the content of the observed field
16 (String message) {
17 _scaffoldKey.currentState.showSnackBar(
18 SnackBar(
19 content: Text(message),
20 ),
21 );
22 },
23 ),
24 ];
25 }
26
27 @override
28 void dispose() {
29 _disposers.forEach((d) => d());
30 super.dispose();
31 }
32
33 @override
34 Widget build(BuildContext context) {
35 return Scaffold(
36 key: _scaffoldKey,
37 appBar: AppBar(
38 title: Text("Recherche un climat"),
39 ),
40 ...
41 );
42 }
43...
44}
C-2. un Observer
pour reconstruire l’UI
Pas grand chose à dire , pour l’objet Observer
. Il renvoie un Widget
et s’exécute chaque fois qu’il ya un changement d’état.
Nous allons observé la propriété state
. Ce qui nous permettra de savoir l’état global de l’application (initial , en cours de chargement ou chargée ).
1 @override
2 Widget build(BuildContext context) {
3 return Scaffold(
4 key: _scaffoldKey,
5 appBar: AppBar(
6 title: Text("Recherche un climat"),
7 ),
8 body: Container(
9 padding: EdgeInsets.symmetric(vertical: 16),
10 alignment: Alignment.center,
11 child: Observer(
12 builder: (_) {
13 switch (_weatherStore.state) {
14 case StoreState.initial:
15 return buildInitialInput();
16 case StoreState.loading:
17 return buildLoading();
18 case StoreState.loaded:
19 return buildColumnWithData(_weatherStore.weather);
20 }
21 },
22 ),
23 ),
24 );
25 }
26
D. Conclusion, mise en place de MobX
Avec ce post , vous avez le détail pour gérer l’état de votre application avec MobX. Il ya beaucoup de framework pour la gestion de l’état de votre application en Flutter. Chaque développeur a un peu sa préférence. Pour personnellement j’aime bien l’approche de BLoC, mais contrairement à MobX on peut se retrouver à gérer beaucoup de fichiers.
La génération de code qui masque la petite complicité du framework Mobx est un peu plus.