Flutter-Tutorial: So erstellen Sie Ihre erste Flutter-App
Veröffentlicht: 2022-03-11Was ist Flattern?
Flutter ist Googles SDK für die Entwicklung mobiler Apps, mit dem Ihr Produkt gleichzeitig auf Android- und iOS-Plattformen abzielen kann, ohne dass zwei separate Codebasen verwaltet werden müssen. Darüber hinaus können Apps, die Flutter verwenden, auch für das kommende Fuchsia-Betriebssystem von Google kompiliert werden.
Flutter hat kürzlich einen wichtigen Meilenstein erreicht – die stabile Version 1.0. Die Veröffentlichung fand am 5. Dezember 2018 in London beim Flutter Live-Event statt. Obwohl es immer noch als ein frühes und sich entwickelndes Softwareunternehmen angesehen werden kann, konzentriert sich dieser Artikel auf ein bereits bewährtes Konzept und zeigt, wie man eine voll funktionsfähige Messaging-App entwickelt, die auf beide großen mobilen Plattformen mit Flutter 1.2 und Firebase abzielt.
Wie aus der folgenden Grafik ersichtlich ist, hat Flutter in den letzten Monaten viele Benutzer gewonnen. Im Jahr 2018 verdoppelte sich der Marktanteil von Flutter und es ist auf dem besten Weg, React Native in Bezug auf Suchanfragen zu übertreffen, daher unsere Entscheidung, ein neues Flutter-Tutorial zu erstellen.
Hinweis: Dieser Artikel konzentriert sich nur auf bestimmte Teile der Implementierung. Die vollständige Quellcodereferenz für das Projekt finden Sie in diesem GitHub-Repository.
Voraussetzungen
Obwohl Anstrengungen unternommen wurden, um es den Lesern zu ermöglichen, dieses Projekt zu verfolgen und durchzuführen, auch wenn es ihr erster Versuch einer mobilen Entwicklung ist, werden viele Kernkonzepte der mobilen Entwicklung, die nicht Flutter-spezifisch sind, erwähnt und ohne detaillierte Erklärung verwendet.
Dies wurde aus Gründen der Kürze des Artikels vorgenommen, da eines seiner Ziele darin besteht, dass der Leser das Projekt in einer Sitzung abschließt. Schließlich geht der Artikel davon aus, dass Sie Ihre Entwicklungsumgebung bereits eingerichtet haben, einschließlich der erforderlichen Android Studio-Plugins und des Flutter SDK.
Firebase einrichten
Das Einrichten von Firebase ist das einzige, was wir für jede Plattform unabhängig tun müssen. Stellen Sie zunächst sicher, dass Sie im Firebase-Dashboard ein neues Projekt erstellen und Android- und iOS-Anwendungen im neu generierten Arbeitsbereich hinzufügen. Die Plattform erstellt zwei Konfigurationsdateien, die Sie herunterladen müssen: google-services.json
für Android und GoogleService-Info.plist
für iOS. Stellen Sie vor dem Schließen des Dashboards sicher, dass Sie Firebase- und Google-Authentifizierungsanbieter aktivieren, da wir sie zur Benutzeridentifikation verwenden. Wählen Sie dazu im Menü den Punkt Authentifizierung und dann die Registerkarte Anmeldemethode.
Jetzt können Sie das Dashboard schließen, während der Rest der Einrichtung in unserer Codebasis stattfindet. Zuerst müssen wir die heruntergeladenen Dateien in unser Projekt einfügen. Die Datei „ google-services.json
“ sollte im Ordner „ $(FLUTTER_PROJECT_ROOT)/android/app
“ und GoogleService-Info.plist
“ im Verzeichnis „ $(FLUTTER_PROJECT_ROOT)/ios/Runner
werden. Als Nächstes müssen wir die Firebase-Bibliotheken, die wir im Projekt verwenden werden, tatsächlich einrichten und sie mit den Konfigurationsdateien verbinden. Dies geschieht durch Angabe der Dart-Pakete (Bibliotheken), die wir in der pubspec.yaml
-Datei unseres Projekts verwenden werden. Fügen Sie im Abschnitt "Abhängigkeiten" der Datei das folgende Snippet ein:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
Die ersten beiden haben nichts mit Firebase zu tun, werden aber häufig im Projekt verwendet. Die letzten beiden sind hoffentlich selbsterklärend.
Schließlich müssen wir plattformspezifische Projekteinstellungen konfigurieren, mit denen unser Authentifizierungsablauf erfolgreich abgeschlossen werden kann. Auf der Android-Seite müssen wir das Google-Services-Gradle-Plugin zu unserer Gradle-Konfiguration auf Projektebene hinzufügen. Mit anderen Worten, wir müssen das folgende Element zur Abhängigkeitsliste in der Datei $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
Dann müssen wir dieses Plugin anwenden, indem wir diese Zeile am Ende von $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
Als Letztes müssen Sie für diese Plattform Ihre Facebook-Anwendungsparameter eingeben. Was wir hier suchen, ist die Bearbeitung dieser beiden Dateien - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml
und $(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>
Jetzt ist es Zeit für iOS. Glücklicherweise müssen wir in diesem Fall nur eine Datei ändern. Fügen Sie $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
die folgenden Werte hinzu (beachten Sie, dass das Element CFBundleURLTypes
möglicherweise bereits in der Liste vorhanden ist; in diesem Fall müssen Sie diese Elemente dem vorhandenen Array hinzufügen, anstatt es erneut zu deklarieren). Datei:
<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>
Ein Wort zur BLoC-Architektur
Dieser Architekturstandard wurde in einem unserer vorherigen Artikel beschrieben, der die Verwendung von BLoC für die gemeinsame Nutzung von Code in Flutter und AngularDart demonstriert, daher werden wir ihn hier nicht im Detail erläutern.
Die Grundidee hinter der Hauptidee ist, dass jeder Bildschirm die folgenden Klassen hat: - Ansicht - die dafür verantwortlich ist, den aktuellen Zustand anzuzeigen und Benutzereingaben als Ereignisse an den Block zu delegieren. - Status - stellt „Live“-Daten dar, mit denen der Benutzer in der aktuellen Ansicht interagiert. - bloc - reagiert auf Ereignisse und aktualisiert den Status entsprechend, wobei optional Daten von einem oder mehreren lokalen oder entfernten Repositories angefordert werden. - Ereignis - das ist ein bestimmtes Aktionsergebnis, das den aktuellen Zustand ändern kann oder nicht.
Als grafische Darstellung kann man sich das so vorstellen:
Darüber hinaus haben wir ein Modellverzeichnis , das Datenklassen und Repositories enthält, die Instanzen dieser Klassen erzeugen.
UI-Entwicklung
Die Erstellung der Benutzeroberfläche mit Flutter erfolgt vollständig in Dart, im Gegensatz zur nativen App-Entwicklung in Android und iOS, bei der die Benutzeroberfläche mithilfe des XML-Schemas erstellt wird und vollständig von der Codebasis der Geschäftslogik getrennt ist. Wir werden relativ einfache UI-Element-Kompositionen mit unterschiedlichen Komponenten basierend auf dem aktuellen Zustand verwenden (z. B. isLoading, isEmpty-Parameter). Die Benutzeroberfläche in Flutter dreht sich um Widgets oder besser gesagt um den Widget-Baum. Widgets können entweder zustandslos oder zustandsbehaftet sein. Wenn es um zustandsbehaftete geht, ist es wichtig zu betonen, dass, wenn setState()
für ein bestimmtes Widget aufgerufen wird, das gerade angezeigt wird (ein Aufruf im Konstruktor oder nachdem es verworfen wurde, zu einem Laufzeitfehler führt), ein Build- und Draw-Durchlauf ist soll im nächsten Ziehzyklus durchgeführt werden.
Der Kürze halber zeigen wir hier nur eine der UI-Klassen (Ansicht):

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); } }
Der Rest der UI-Klassen folgt den gleichen Mustern, hat aber möglicherweise unterschiedliche Aktionen und kann zusätzlich zum Ladestatus einen leeren Status-Widget-Baum aufweisen.
Authentifizierung
Wie Sie vielleicht schon erraten haben, verwenden wir die Bibliotheken google_sign_in
und flutter_facebook_login
, um den Benutzer zu authentifizieren, indem wir uns auf sein soziales Netzwerkprofil verlassen. Stellen Sie zunächst sicher, dass Sie diese Pakete in die Datei importieren, die die Anmeldeanforderungslogik verarbeiten wird:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
Jetzt werden wir zwei unabhängige Teile haben, die sich um unseren Authentifizierungsablauf kümmern. Der erste wird entweder eine Facebook- oder Google-Anmeldeanfrage initiieren:
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)); } }
Der zweite wird aufgerufen, wenn wir die Profildaten von einem der beiden Anbieter erhalten. Wir werden dies erreichen, indem wir unseren Login-Handler anweisen, den firebase_auth onAuthStateChange
Stream abzuhören:
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)); }); } }
Die UserRepo
und LoginRepo
Implementierung wird hier nicht veröffentlicht, aber Sie können sich gerne das GitHub-Repo als vollständige Referenz ansehen.
Flutter-Tutorial: So erstellen Sie eine Instant Messaging-App
Schließlich kommen wir zum interessanten Teil. Wie der Name schon sagt, sollen die Nachrichten so schnell wie möglich ausgetauscht werden, idealerweise sofort . Glücklicherweise erlaubt uns cloud_firestore
, mit der Firestore-Instanz zu interagieren, und wir können ihre snapshots()
Funktion verwenden, um einen Datenstrom zu öffnen, der uns Updates in Echtzeit liefert. Meiner Meinung nach ist der gesamte chat_repo
-Code ziemlich einfach, mit Ausnahme der startChatroomForUsers
Methode. Es ist für die Erstellung eines neuen Chatrooms für zwei Benutzer verantwortlich, es sei denn, es gibt einen bestehenden Chatroom, der beide Benutzer enthält (da wir nicht mehrere Instanzen desselben Benutzerpaars haben möchten). In diesem Fall gibt es den vorhandenen Chatroom zurück.
Aufgrund des Designs von Firestore werden derzeit jedoch keine verschachtelten array-contains
Abfragen unterstützt. Daher können wir den entsprechenden Datenstrom nicht abrufen, sondern müssen eine zusätzliche Filterung auf unserer Seite vornehmen. Diese Lösung besteht darin, alle Chatrooms für den angemeldeten Benutzer abzurufen und dann nach dem zu suchen, der auch den ausgewählten Benutzer enthält:
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); } }
Außerdem unterstützt Firebase aufgrund ähnlicher Designeinschränkungen derzeit keine Array-Aktualisierungen (Einfügen eines neuen Elements in einen vorhandenen Array-Feldwert) mit einem speziellen FieldValue.serverTimestamp()
Wert.
Dieser Wert zeigt der Plattform an, dass das Feld, das diesen anstelle eines tatsächlichen Werts enthält, mit dem tatsächlichen Zeitstempel auf dem Server zum Zeitpunkt der Transaktion gefüllt werden sollte. Stattdessen verwenden wir DateTime.now()
in dem Moment, in dem wir unser neues serialisiertes Nachrichtenobjekt erstellen und dieses Objekt in die Nachrichtensammlung des Chatrooms einfügen.
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; } }
Einpacken
Offensichtlich ist die von uns entwickelte Flutter-Messaging-App eher ein Proof-of-Concept als eine marktreife Instant-Messaging-Anwendung. Als Ideen für die Weiterentwicklung könnte man die Einführung einer Ende-zu-Ende-Verschlüsselung oder von Rich Content (Gruppenchats, Medienanhänge, URL-Parsing) in Erwägung ziehen. Aber vor all dem sollte man Push-Benachrichtigungen implementieren, da sie so ziemlich ein Muss für eine Instant-Messaging-Anwendung sind, und wir haben sie der Kürze halber aus dem Rahmen dieses Artikels entfernt. Darüber hinaus fehlen Firestore noch einige Funktionen, um einfachere und genauere Daten wie verschachtelte array-contains
Abfragen zu haben.
Wie zu Beginn des Artikels erwähnt, ist Flutter erst kürzlich zur stabilen 1.0-Version gereift und wird weiter wachsen, nicht nur in Bezug auf Framework-Funktionen und -Fähigkeiten, sondern auch in Bezug auf die Entwicklungsgemeinschaft und Bibliotheken von Drittanbietern und Ressourcen. Es ist sinnvoll, jetzt Ihre Zeit zu investieren, um sich mit der Flutter-App-Entwicklung vertraut zu machen, da sie eindeutig hier bleibt und Ihren mobilen Entwicklungsprozess beschleunigt.
Ohne zusätzliche Kosten sind Flutter-Entwickler auch bereit, Googles aufstrebendes OS – Fuchsia – ins Visier zu nehmen.