Tutoriel Flutter : comment créer votre première application Flutter

Publié: 2022-03-11

Qu'est-ce que Flutter ?

Flutter est le SDK de développement d'applications mobiles de Google qui permet à votre produit de cibler simultanément les plates-formes Android et iOS, sans avoir besoin de maintenir deux bases de code distinctes. De plus, les applications utilisant Flutter peuvent également être compilées pour cibler le prochain système d'exploitation Fuchsia de Google.

Flutter a récemment franchi une étape majeure - la version stable 1.0. La sortie a eu lieu à Londres, le 5 décembre 2018, lors de l'événement Flutter Live. Bien qu'il puisse encore être considéré comme une entreprise logicielle précoce et évolutive, cet article se concentrera sur un concept déjà éprouvé et montrera comment développer une application de messagerie entièrement fonctionnelle qui cible les deux principales plates-formes mobiles utilisant Flutter 1.2 et Firebase.

Comme le montre le tableau ci-dessous, Flutter a gagné de nombreux utilisateurs ces derniers mois. En 2018, la part de marché de Flutter a doublé et il est en passe de surpasser React Native en termes de requêtes de recherche, d'où notre décision de créer un nouveau tutoriel Flutter.

Graphique comparant les utilisateurs de Flutter et React de juillet à septembre 2018.

Remarque : cet article se concentre uniquement sur certains éléments de l'implémentation. La référence complète du code source du projet est disponible dans ce dépôt GitHub.

Conditions préalables

Même si des efforts ont été faits pour permettre aux lecteurs de suivre et d'accomplir ce projet même s'il s'agit de leur première tentative de développement mobile, de nombreux concepts de développement mobile de base qui ne sont pas spécifiques à Flutter sont mentionnés et utilisés sans explication détaillée.

Cela a été entrepris pour la brièveté de l'article car l'un de ses objectifs est que le lecteur termine le projet en une seule séance. Enfin, l'article suppose que vous avez déjà configuré votre environnement de développement, y compris les plug-ins Android Studio requis et le SDK Flutter.

Configuration de la base de feu

La configuration de Firebase est la seule chose que nous devons faire indépendamment pour chaque plate-forme. Tout d'abord, assurez-vous de créer un nouveau projet dans le tableau de bord Firebase et d'ajouter des applications Android et iOS dans l'espace de travail nouvellement généré. La plateforme produira deux fichiers de configuration que vous devrez télécharger : google-services.json pour Android et GoogleService-Info.plist pour iOS. Avant de fermer le tableau de bord, assurez-vous d'activer les fournisseurs d'authentification Firebase et Google, car nous les utiliserons pour l'identification des utilisateurs. Pour ce faire, choisissez l'élément Authentification dans le menu, puis sélectionnez l'onglet Méthode de connexion.

Vous pouvez maintenant fermer le tableau de bord pendant que le reste de la configuration se déroule dans notre base de code. Tout d'abord, nous devons mettre les fichiers que nous avons téléchargés dans notre projet. Le fichier google-services.json doit être placé dans le dossier $(FLUTTER_PROJECT_ROOT)/android/app et GoogleService-Info.plist doit être placé dans le $(FLUTTER_PROJECT_ROOT)/ios/Runner . Ensuite, nous devons réellement configurer les bibliothèques Firebase que nous allons utiliser dans le projet et les associer aux fichiers de configuration. Cela se fait en spécifiant les packages Dart (bibliothèques) que nous utiliserons dans le fichier pubspec.yaml de notre projet. Dans la section des dépendances du fichier, collez l'extrait de code suivant :

 flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:

Les deux premiers ne sont pas liés à Firebase mais seront fréquemment utilisés dans le projet. Les deux derniers sont, espérons-le, explicites.

Enfin, nous devons configurer les paramètres de projet spécifiques à la plate-forme qui permettront à notre flux d'authentification de se terminer avec succès. Du côté Android, nous devons ajouter le plugin google-services Gradle à notre configuration Gradle au niveau du projet. En d'autres termes, nous devons ajouter l'élément suivant à la liste des dépendances dans le fichier $(FLUTTER_PROJECT_ROOT)/android/build.gradle :

 classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version

Ensuite, nous devons appliquer ce plugin en ajoutant cette ligne à la fin de $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle :

 apply plugin: 'com.google.gms.google-services'

La dernière chose pour cette plateforme est d'enrôler vos paramètres d'application Facebook. Ce que nous recherchons ici est de modifier ces deux fichiers - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml et $(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml :

 <!-- AndroidManifest.xml --> <manifest xmlns:androcom.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> <activity android:name="com.facebook.CustomTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/fb_login_protocol_scheme" /> </intent-filter> </activity> <!-- … --> </application> </manifest> <!-- strings.xml --> <resources> <string name="app_name">Toptal Chat</string> <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string> <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string> </resources>

Il est maintenant temps pour iOS. Heureusement, nous n'avons besoin de changer qu'un seul fichier dans ce cas. Ajoutez les valeurs suivantes (notez que l'élément CFBundleURLTypes peut déjà exister dans la liste ; dans ce cas, vous devez ajouter ces éléments au tableau existant au lieu de le déclarer à nouveau) à $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist fichier:

 <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_FACEBOOK_URL}</string> </array> </dict> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}</string> </array> </dict> </array> <key>FacebookAppID</key> <string>${YOUR_FACEBOOK_APP_ID}</string> <key>FacebookDisplayName</key> <string>${YOUR_FACEBOOK_APP_NAME}</string> <key>LSApplicationQueriesSchemes</key> <array> <string>fbapi</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> </array>

Un mot sur l'architecture BLoC

Cette norme d'architecture a été décrite dans l'un de nos articles précédents, démontrant l'utilisation de BLoC pour le partage de code dans Flutter et AngularDart, nous ne l'expliquerons donc pas en détail ici.

L'idée de base derrière l'idée principale est que chaque écran a les classes suivantes : - view - qui est responsable de l'affichage de l'état actuel et de la délégation des entrées de l'utilisateur en tant qu'événements à bloc. - état - qui représente les données "en direct" avec lesquelles l'utilisateur interagit à l'aide de la vue actuelle. - bloc - qui répond aux événements et met à jour l'état en conséquence, en demandant éventuellement des données à un ou plusieurs référentiels locaux ou distants. - événement - qui est un résultat d'action défini qui peut ou non changer l'état actuel.

En tant que représentation graphique, cela peut être pensé comme ceci :

Tutoriel Flutter : Représentation graphique de l'architecture BLoC.

De plus, nous avons un répertoire de modèles qui contient des classes de données et des référentiels qui produisent des instances de ces classes.

Développement de l'interface utilisateur

La création de l'interface utilisateur à l'aide de Flutter se fait entièrement dans Dart, contrairement au développement d'applications natives dans Android et iOS où l'interface utilisateur est construite à l'aide du schéma XML et est complètement séparée de la base de code de la logique métier. Nous allons utiliser des compositions d'éléments d'interface utilisateur relativement simples avec différents composants basés sur l'état actuel (par exemple, les paramètres isLoading, isEmpty). L'interface utilisateur de Flutter tourne autour des widgets, ou plutôt de l'arborescence des widgets. Les widgets peuvent être sans état ou avec état. En ce qui concerne les états, il est important de souligner que, lorsque setState() est appelé sur un widget particulier actuellement affiché (l'appeler dans le constructeur ou après sa suppression entraîne une erreur d'exécution), une passe de construction et de dessin est doit être effectué lors du prochain cycle de dessin.

Par souci de concision, nous n'afficherons ici qu'une seule des classes d'interface utilisateur (vue) :

 class LoginScreen extends StatefulWidget { LoginScreen({Key key}) : super(key: key); @override State<StatefulWidget> createState() => _LoginState(); } class _LoginState extends State<LoginScreen> { final _bloc = LoginBloc(); @override Widget build(BuildContext context) { return BlocProvider<LoginBloc>( bloc: _bloc, child: LoginWidget(widget: widget, widgetState: this) ); } @override void dispose() { _bloc.dispose(); super.dispose(); } } class LoginWidget extends StatelessWidget { const LoginWidget({Key key, @required this.widget, @required this.widgetState}) : super(key: key); final LoginScreen widget; final _LoginState widgetState; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: BlocBuilder( bloc: BlocProvider.of<LoginBloc>(context), builder: (context, LoginState state) { if (state.loading) { return Center( child: CircularProgressIndicator(strokeWidth: 4.0) ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginGoogle(this), child: Text( "Login with Google", style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ), ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginFacebook(this), child: Text( "Login with Facebook", style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ), ), ], ), ); } }), ); } void navigateToMain() { NavigationHelper.navigateToMain(widgetState.context); } }

Les autres classes d'interface utilisateur suivent les mêmes modèles, mais ont peut-être des actions différentes et peuvent comporter une arborescence de widgets d'état vide en plus de l'état de chargement.

Authentification

Comme vous l'avez peut-être deviné, nous utiliserons les bibliothèques google_sign_in et flutter_facebook_login pour authentifier l'utilisateur en nous appuyant sur son profil de réseau social. Tout d'abord, assurez-vous d'importer ces packages dans le fichier qui va gérer la logique de demande de connexion :

 import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';

Maintenant, nous allons avoir deux parties indépendantes qui vont s'occuper de notre flux d'authentification. Le premier va lancer une demande de connexion Facebook ou Google :

 void onLoginGoogle(LoginWidget view) async { dispatch(LoginEventInProgress()); final googleSignInRepo = GoogleSignIn(signInOption: SignInOption.standard, scopes: ["profile", "email"]); final account = await googleSignInRepo.signIn(); if (account != null) { LoginRepo.getInstance().signInWithGoogle(account); } else { dispatch(LogoutEvent()); } } void onLoginFacebook(LoginWidget view) async { dispatch(LoginEventInProgress()); final facebookSignInRepo = FacebookLogin(); final signInResult = await facebookSignInRepo.logInWithReadPermissions(["email"]); if (signInResult.status == FacebookLoginStatus.loggedIn) { LoginRepo.getInstance().signInWithFacebook(signInResult); } else if (signInResult.status == FacebookLoginStatus.cancelledByUser) { dispatch(LogoutEvent()); } else { dispatch(LoginErrorEvent(signInResult.errorMessage)); } }

Le second sera appelé lorsque nous obtiendrons les données de profil de l'un ou l'autre fournisseur. Nous allons y parvenir en demandant à notre gestionnaire de connexion d'écouter le firebase_auth onAuthStateChange :

 void _setupAuthStateListener(LoginWidget view) { if (_authStateListener == null) { _authStateListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) { if (user != null) { final loginProvider = user.providerId; UserRepo.getInstance().setCurrentUser(User.fromFirebaseUser(user)); if (loginProvider == "google") { // TODO analytics call for google login provider } else { // TODO analytics call for facebook login provider } view.navigateToMain(); } else { dispatch(LogoutEvent()); } }, onError: (error) { dispatch(LoginErrorEvent(error)); }); } }

L'implémentation de UserRepo et LoginRepo ne sera pas publiée ici, mais n'hésitez pas à consulter le référentiel GitHub pour une référence complète.

Tutoriel Flutter : comment créer une application de messagerie instantanée

Enfin, nous arrivons à la partie intéressante. Comme son nom l'indique, les messages doivent être échangés le plus rapidement possible, idéalement, cela devrait être instantané . Heureusement, cloud_firestore nous permet d'interagir avec l'instance Firestore et nous pouvons utiliser sa fonctionnalité snapshots() pour ouvrir un flux de données qui nous donnera des mises à jour en temps réel. À mon avis, tout le code chat_repo est assez simple à l'exception de la méthode startChatroomForUsers . Il est responsable de la création d'un nouveau salon de discussion pour deux utilisateurs à moins qu'il n'en existe un existant qui contient les deux utilisateurs (car nous ne voulons pas avoir plusieurs instances de la même paire d'utilisateurs), auquel cas il renvoie le salon de discussion existant.

Cependant, en raison de la conception de Firestore, il ne prend actuellement pas en charge les requêtes imbriquées array-contains . Nous ne pouvons donc pas récupérer le flux de données approprié mais devons effectuer un filtrage supplémentaire de notre côté. Cette solution consiste à récupérer tous les salons de discussion de l'utilisateur connecté puis à rechercher celui qui contient également l'utilisateur sélectionné :

 Future<SelectedChatroom> startChatroomForUsers(List<User> users) async { DocumentReference userRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[1].uid); QuerySnapshot queryResults = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .where("participants", arrayContains: userRef) .getDocuments(); DocumentReference otherUserRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[0].uid); DocumentSnapshot roomSnapshot = queryResults.documents.firstWhere((room) { return room.data["participants"].contains(otherUserRef); }, orElse: () => null); if (roomSnapshot != null) { return SelectedChatroom(roomSnapshot.documentID, users[0].displayName); } else { Map<String, dynamic> chatroomMap = Map<String, dynamic>(); chatroomMap["messages"] = List<String>(0); List<DocumentReference> participants = List<DocumentReference>(2); participants[0] = otherUserRef; participants[1] = userRef; chatroomMap["participants"] = participants; DocumentReference reference = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .add(chatroomMap); DocumentSnapshot chatroomSnapshot = await reference.get(); return SelectedChatroom(chatroomSnapshot.documentID, users[0].displayName); } }

De plus, en raison de contraintes de conception similaires, Firebase ne prend actuellement pas en charge les mises à jour de tableau (insertion d'un nouvel élément dans une valeur de champ de tableau existante) avec la valeur spéciale FieldValue.serverTimestamp() .

Cette valeur indique à la plate-forme que le champ qui contient ceci au lieu d'une valeur réelle doit être rempli avec l'horodatage réel sur le serveur au moment où la transaction a lieu. Au lieu de cela, nous utilisons DateTime.now() au moment où nous créons notre nouvel objet sérialisé de message et insérons cet objet dans la collection de messages de la salle de discussion.

 Future<bool> sendMessageToChatroom(String chatroomId, User user, String message) async { try { DocumentReference authorRef = _firestore.collection(FirestorePaths.USERS_COLLECTION).document(user.uid); DocumentReference chatroomRef = _firestore.collection(FirestorePaths.CHATROOMS_COLLECTION).document(chatroomId); Map<String, dynamic> serializedMessage = { "author" : authorRef, "timestamp" : DateTime.now(), "value" : message }; chatroomRef.updateData({ "messages" : FieldValue.arrayUnion([serializedMessage]) }); return true; } catch (e) { print(e.toString()); return false; } }

Emballer

De toute évidence, l'application de messagerie Flutter que nous avons développée est plus une preuve de concept qu'une application de messagerie instantanée prête pour le marché. Comme idées de développement ultérieur, on pourrait envisager d'introduire un chiffrement de bout en bout ou un contenu riche (discussions de groupe, pièces jointes multimédias, analyse d'URL). Mais avant tout cela, il faut implémenter les notifications push car elles sont à peu près une fonctionnalité incontournable pour une application de messagerie instantanée, et nous l'avons retirée du cadre de cet article par souci de brièveté. De plus, Firestore manque encore de quelques fonctionnalités afin d'avoir des requêtes array-contains imbriqués plus simples et plus précises.

Comme mentionné au début de l'article, Flutter n'a mûri que récemment dans la version 1.0 stable et va continuer à croître, non seulement en ce qui concerne les fonctionnalités et les capacités du framework, mais aussi en ce qui concerne la communauté de développement et les bibliothèques tierces et Ressources. Il est logique d'investir votre temps pour vous familiariser avec le développement d'applications Flutter maintenant, car il est clairement là pour rester et accélérer votre processus de développement mobile.

Sans frais supplémentaires, les développeurs de Flutter seront également prêts à cibler le système d'exploitation émergent de Google, Fuchsia.

En relation : Le langage Dart : lorsque Java et C# ne sont pas assez pointus