
Principes généraux
Flutter est un cadre réactif, et pour un développeur spécialisé dans le développement natif, sa philosophie peut être inhabituelle. Par conséquent, nous commençons par un bref examen.
L'interface utilisateur sur Flutter, comme dans la plupart des frameworks modernes, se compose d'une arborescence de composants (widgets). Lorsqu'un composant change, celui-ci et tous ses composants enfants sont restitués (avec des optimisations internes, décrites ci-dessous). Lorsque l'affichage change globalement (par exemple, en tournant l'écran), l'arborescence entière du widget est redessinée.
Cette approche peut sembler inefficace, mais en fait, elle donne au programmeur le contrôle de la vitesse de travail. Si vous mettez à jour l'interface au plus haut niveau sans avoir besoin, tout fonctionnera lentement, mais avec la disposition correcte des widgets, les applications sur Flutter peuvent être très rapides.
Flutter a deux types de widgets - sans état et avec état. Les premiers (analogues à Pure Components in React) n'ont pas d'état et sont entièrement décrits par leurs paramètres. Si les conditions d'affichage ne changent pas (par exemple, la taille de la zone dans laquelle le widget doit être affiché) et ses paramètres, le système réutilise la représentation visuelle précédemment créée du widget, donc l'utilisation de widgets sans état a un bon effet sur les performances. Dans le même temps, de toute façon, chaque fois que le widget est redessiné, un nouvel objet est formellement créé et le constructeur est lancé.
Les widgets avec état conservent un certain état entre les rendus. Pour ce faire, ils sont décrits par deux classes. La première des classes, le widget lui-même, décrit les objets créés lors de chaque rendu. La deuxième classe décrit l'état du widget et ses objets sont transférés vers les objets widget créés. Les widgets d'état avec état sont une source majeure de refonte de l'interface. Pour ce faire, vous devez modifier ses propriétés dans l'appel à la méthode SetState. Ainsi, contrairement à de nombreux autres frameworks, Flutter n'a pas de suivi d'état implicite - tout changement dans les propriétés du widget en dehors de la méthode SetState ne conduit pas à redessiner l'interface.
Maintenant, après avoir décrit les bases, vous pouvez commencer avec une application simple qui utilise des widgets sans état et avec état:
Application de baseimport 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } }
Exemple complet
Si vous avez besoin de conditions plus tenaces
Continuons. L'état dynamique des widgets est maintenu entre les interfaces de redessin, mais uniquement aussi longtemps que le widget est nécessaire, c'est-à-dire vraiment situé sur l'écran. Faisons une expérience simple - placez notre liste sur l'onglet:
Application Tab class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } }
Exemple complet
Au démarrage, vous pouvez voir que lors du basculement entre les onglets, l'état est supprimé (la méthode dispose () est appelée), lorsqu'il est renvoyé, il est recréé (la méthode initState ()). Ceci est raisonnable, car le stockage de l'état des widgets non affichables consommera des ressources système. Dans le cas où l'état du widget doit survivre à sa dissimulation complète, plusieurs approches sont possibles:
Tout d'abord, vous pouvez utiliser des objets distincts (ViewModel) pour stocker l'état. Dart au niveau de la langue prend en charge les constructeurs d'usine qui peuvent être utilisés pour créer des usines et des singletones qui stockent les données nécessaires.
J'aime plus cette approche, car Il vous permet d'isoler la logique métier de l'interface utilisateur. Cela est particulièrement vrai en raison du fait que Flutter Release Preview 2 a ajouté la possibilité de créer des interfaces parfaites pour les pixels pour iOS, mais vous devez le faire, bien sûr, sur les widgets correspondants.
Deuxièmement, il est possible d'utiliser l'approche d'élévation d'état, familière aux programmeurs React, lorsque les données sont stockées dans des composants situés en amont. Étant donné que Flutter redessine l'interface uniquement lorsque la méthode setState () est appelée, ces données peuvent être modifiées et utilisées sans rendu. Cette approche est un peu plus complexe et augmente la connectivité des widgets dans la structure, mais vous permet de spécifier le niveau de stockage des données de manière ponctuelle.
Enfin, il existe des bibliothèques de stockage d'état comme flutter_redux .
Pour simplifier, nous utilisons la première approche. Créons une classe ListData distincte, singleton, qui stocke les valeurs de notre liste. Lors de l'affichage, nous utiliserons cette classe.
Application de récupération de données à onglets class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } }
Exemple complet
Enregistrement d'une position de défilement
Si vous faites défiler la liste de l'exemple précédent, puis basculez entre les onglets, il est facile de remarquer que la position de défilement n'est pas enregistrée. C'est logique, car il n'est pas stocké dans notre classe ListData et le propre état du widget ne survit pas à la commutation entre les onglets. Nous implémenterons le stockage de l'état de défilement manuellement, mais pour le plaisir, nous ne l'ajouterons pas à une classe distincte et non à ListData, mais à un état de niveau supérieur pour montrer comment travailler avec.
Faites attention aux widgets ScrollController et NotificationListener (ainsi qu'aux DefaultTabController précédemment utilisés). Le concept de widgets qui n'ont pas leur propre affichage devrait être familier aux développeurs travaillant avec React / Redux - les composants du conteneur sont activement utilisés dans ce bundle. Dans Flutter, les widgets non affichés sont couramment utilisés pour ajouter des fonctionnalités aux widgets enfants. Cela vous permet de laisser les widgets visuels eux-mêmes légers et de ne pas traiter les événements système là où ils ne sont pas nécessaires.
Le code est basé sur la solution proposée par Marcin Szałek chez Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ). Le plan est le suivant:
- Ajoutez un ScrollController à la liste pour travailler avec la position de défilement.
- Ajoutez NotificationListener à la liste pour passer l'état de défilement.
- Nous enregistrons la position du défilement dans _MyHomePageState (qui est un niveau au-dessus des onglets) et l'associons au défilement de la liste.
Application avec sauvegarde de la position de défilement class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } }
Exemple complet
Vivre l'arrêt de l'application
La sauvegarde des informations pendant la durée de l'application est bonne, mais vous souhaitez souvent les sauvegarder entre les sessions, en particulier compte tenu de l'habitude des systèmes d'exploitation de fermer les applications en arrière-plan lorsque la mémoire est insuffisante. Les principales options de stockage de données persistantes dans Flutter sont les suivantes:
- Les préférences partagées ( https://pub.dartlang.org/packages/shared_preferences ) sont un wrapper autour de NSUserDefaults (sur iOS) et SharedPreferences (sur Android) et vous permettent de stocker un petit nombre de paires clé-valeur. Idéal pour stocker des paramètres.
- sqflite ( https://pub.dartlang.org/packages/sqflite ) est un plugin pour travailler avec SQLite (avec quelques limitations). Prend en charge les requêtes de bas niveau et les assistants. De plus, par analogie avec Room, il vous permet de travailler avec des versions de schéma de base de données et de définir le code de mise à jour du schéma lors de la mise à jour de l'application.
- Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) fait partie de la famille de plugins FireBase officielle.
Pour illustrer cela, nous allons enregistrer l'état de défilement dans les préférences partagées. Pour ce faire, ajoutez la restauration de la position de défilement lors de l'initialisation de l'état _MyHomePageState et de l'enregistrement lors du défilement.
Ici, nous devons nous attarder sur le modèle asynchrone Flutter / Dart, car tous les services externes fonctionnent sur les appels asynchrones. Le principe de fonctionnement de ce modèle est similaire à node.js - il existe un thread d'exécution principal (thread), qui est interrompu par des appels asynchrones. À chaque interruption suivante (et l'interface utilisateur les rend constamment), les résultats des opérations asynchrones terminées sont traités. En même temps, il est possible d'exécuter de lourds calculs dans les threads d'arrière-plan (via la fonction de calcul).
Ainsi, l'écriture et la lecture dans SharedPreferences se font de manière asynchrone (bien que la bibliothèque permette une lecture synchrone à partir du cache). Pour commencer, nous traiterons de la lecture. L'approche standard de la récupération asynchrone des données ressemble à ceci: démarrez le processus asynchrone et, une fois terminé, exécutez SetState en écrivant les valeurs reçues. En conséquence, l'interface utilisateur sera mise à jour en utilisant les données reçues. Cependant, dans ce cas, nous ne travaillons pas avec des données, mais avec la position de défilement. Nous n'avons pas besoin de mettre à jour l'interface, nous avons seulement besoin d'appeler la méthode jumpTo sur le ScrollController. Le problème est que le résultat du traitement d'une demande asynchrone peut revenir à tout moment et ce ne sera pas nécessairement quoi et où faire défiler. Afin d'être assuré d'effectuer une opération sur une interface entièrement initialisée, nous devons ... toujours faire défiler setState.
Nous obtenons quelque chose comme ce code:
Réglage de l'état @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); }
Avec le disque, tout est plus intéressant. Le fait est que dans le processus de défilement, les événements à ce sujet arrivent constamment. Le démarrage de l'enregistrement asynchrone chaque fois que la valeur est modifiée peut provoquer des erreurs d'application. Nous devons traiter uniquement le dernier événement de la chaîne. En termes de programmation réactive, cela s'appelle debounce et nous allons l'utiliser. Dart prend en charge les principales fonctionnalités de la programmation réactive via des flux de données.Nous devrons donc créer un flux à partir des mises à jour de la position de défilement et y souscrire, en le convertissant à l'aide de Debounce. Pour convertir, nous avons besoin de la bibliothèque stream_transform . Comme approche alternative, vous pouvez utiliser RxDart et travailler en termes de ReactiveX.
Il s'avère que le code suivant:
Enregistrement de statut StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); }
Exemple complet