Tutorial Flutter: Cum să creezi prima ta aplicație Flutter
Publicat: 2022-03-11Ce este Flutter?
Flutter este SDK-ul de dezvoltare a aplicațiilor mobile de la Google, care permite produsului dvs. să vizeze ambele platforme Android și iOS simultan, fără a fi nevoie să mențineți două baze de coduri separate. În plus, aplicațiile care folosesc Flutter pot fi compilate și pentru a viza viitorul sistem de operare Fuchsia al Google.
Flutter a atins recent o piatră de hotar majoră - versiunea stabilă 1.0. Lansarea a avut loc la Londra, 5 decembrie 2018, la evenimentul Flutter Live. Deși poate fi încă considerată o afacere software timpurie și în evoluție, acest articol se va concentra pe un concept deja dovedit și va demonstra cum să dezvolte o aplicație de mesagerie complet funcțională care vizează ambele platforme mobile majore folosind Flutter 1.2 și Firebase.
După cum se poate vedea din graficul de mai jos, Flutter a câștigat o mulțime de utilizatori în ultimele luni. În 2018, cota de piață a lui Flutter s-a dublat și este pe cale să depășească React Native în ceea ce privește interogările de căutare, de unde și decizia noastră de a crea un nou tutorial Flutter.
Notă: Acest articol se concentrează numai pe anumite părți ale implementării. Referința completă a codului sursă pentru proiect poate fi găsită în acest depozit GitHub.
Cerințe preliminare
Chiar dacă au fost depuse eforturi pentru a permite cititorilor să urmărească și să realizeze acest proiect, chiar dacă este prima lor încercare de dezvoltare mobilă, o mulțime de concepte de bază de dezvoltare mobilă care nu sunt specifice Flutter sunt menționate și utilizate fără explicații detaliate.
Acest lucru a fost întreprins pentru concizia articolului, deoarece unul dintre obiectivele sale este ca cititorul să finalizeze proiectul într-o singură ședință. În cele din urmă, articolul presupune că aveți deja configurat mediul de dezvoltare, inclusiv pluginurile necesare Android Studio și Flutter SDK.
Configurare Firebase
Configurarea Firebase este singurul lucru pe care trebuie să-l facem independent pentru fiecare platformă. În primul rând, asigurați-vă că creați un nou proiect în Firebase Dashboard și adăugați aplicații Android și iOS în spațiul de lucru nou generat. Platforma va produce două fișiere de configurare pe care trebuie să le descărcați: google-services.json
pentru Android și GoogleService-Info.plist
pentru iOS. Înainte de a închide tabloul de bord, asigurați-vă că activați Firebase și furnizorii de autentificare Google, deoarece îi vom folosi pentru identificarea utilizatorilor. Pentru a face acest lucru, alegeți elementul Autentificare din meniu și apoi selectați fila Metodă de conectare.
Acum puteți închide tabloul de bord, deoarece restul configurării are loc în baza noastră de cod. În primul rând, trebuie să punem fișierele descărcate în proiectul nostru. Fișierul google-services.json
trebuie plasat în $(FLUTTER_PROJECT_ROOT)/android/app
și GoogleService-Info.plist
trebuie plasat în directorul $(FLUTTER_PROJECT_ROOT)/ios/Runner
. Apoi, trebuie să setăm efectiv bibliotecile Firebase pe care le vom folosi în proiect și să le conectăm cu fișierele de configurare. Acest lucru se face prin specificarea pachetelor Dart (biblioteci) pe care le vom folosi în fișierul pubspec.yaml
al proiectului nostru. În secțiunea de dependențe a fișierului, inserați următorul fragment:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
Primele două nu sunt legate de Firebase, dar vor fi utilizate frecvent în proiect. Ultimele două sunt, sperăm, de la sine explicate.
În cele din urmă, trebuie să configuram setările de proiect specifice platformei, care să permită finalizarea cu succes a fluxului nostru de autentificare. Pe partea Android, trebuie să adăugăm pluginul google-services Gradle la configurația noastră Gradle la nivel de proiect. Cu alte cuvinte, trebuie să adăugăm următorul articol la lista de dependențe din fișierul $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
Apoi trebuie să aplicăm acel plugin adăugând această linie la sfârșitul lui $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
Ultimul lucru pentru această platformă este să vă înscrieți parametrii aplicației Facebook. Ceea ce căutăm aici este editarea acestor două fișiere - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml
și $(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>
Acum este timpul pentru iOS. Din fericire, trebuie să schimbăm doar un fișier în acest caz. Adăugați următoarele valori (rețineți că elementul CFBundleURLTypes
poate exista deja în listă; în acest caz, trebuie să adăugați aceste elemente la matricea existentă în loc să îl declarați din nou) la $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
fişier:
<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 cuvânt despre arhitectura BLoC
Acest standard de arhitectură a fost descris într-unul dintre articolele noastre anterioare, demonstrând utilizarea BLoC pentru partajarea codului în Flutter și AngularDart, așa că nu îl vom explica în detaliu aici.
Ideea de bază din spatele ideii principale este că fiecare ecran are următoarele clase: - vizualizare - care este responsabilă de afișarea stării curente și de delegarea introducerii utilizatorului ca evenimente către bloc. - stare - care reprezintă date „în direct” cu care utilizatorul interacționează folosind vizualizarea curentă. - bloc - care răspunde la evenimente și actualizează starea în consecință, solicitând opțional date de la unul sau mai multe depozite locale sau la distanță. - eveniment - care este un rezultat cert al unei acțiuni care poate sau nu schimba starea curentă.
Ca reprezentare grafică, poate fi gândită astfel:
În plus, avem un director model care conține clase de date și depozite care produc instanțe ale acestor clase.
Dezvoltare UI
Crearea interfeței de utilizare folosind Flutter se face complet în Dart, spre deosebire de dezvoltarea aplicației native în Android și iOS, unde interfața de utilizare este construită folosind schema XML și este complet separată de baza de cod de logica de afaceri. Vom folosi compoziții de elemente UI relativ simple cu diferite componente bazate pe starea curentă (de exemplu, parametrii isLoading, isEmpty). Interfața de utilizare din Flutter se învârte în jurul widget-urilor, sau mai degrabă a arborelui widget. Widgeturile pot fi fie apatride, fie cu stare. Când vine vorba de cele cu stare, este important de subliniat că, atunci când setState()
este apelat pe un anumit widget care este afișat în prezent (apelarea acestuia în constructor sau după ce este eliminat are ca rezultat o eroare de execuție), o trecere de compilare și desenare este programată pentru a fi efectuată la următorul ciclu de desenare.
Pentru concizie, vom afișa aici doar una dintre clasele UI (vizualizare):
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); } }
Restul claselor de UI urmează aceleași modele, dar poate au acțiuni diferite și ar putea prezenta un arbore widget de stare gol pe lângă starea de încărcare.

Autentificare
După cum probabil ați ghicit, vom folosi bibliotecile google_sign_in
și flutter_facebook_login
pentru a autentifica utilizatorul bazându-ne pe profilul său de rețea socială. Mai întâi de toate, asigurați-vă că importați aceste pachete în fișierul care va gestiona logica cererii de conectare:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
Acum, vom avea două părți independente care se vor ocupa de fluxul nostru de autentificare. Prima va iniția fie o solicitare de conectare la Facebook, fie la 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)); } }
Al doilea va fi apelat când primim datele de profil de la oricare furnizor. Vom realiza acest lucru dând instrucțiuni managerului nostru de conectare să asculte fluxul 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)); }); } }
Implementarea UserRepo
și LoginRepo
nu va fi postată aici, dar nu ezitați să aruncați o privire la depozitul GitHub pentru referință completă.
Tutorial Flutter: Cum să construiți o aplicație de mesagerie instantanee
În sfârșit, ajungem la partea interesantă. După cum sugerează și numele, mesajele ar trebui schimbate cât mai repede posibil, în mod ideal, aceasta ar trebui să fie instantanee . Din fericire, cloud_firestore
ne permite să interacționăm cu instanța Firestore și putem folosi caracteristica sa snapshots()
pentru a deschide un flux de date care ne va oferi actualizări în timp real. În opinia mea, tot codul chat_repo
este destul de simplu, cu excepția metodei startChatroomForUsers
. Este responsabil pentru crearea unei noi camere de chat pentru doi utilizatori, cu excepția cazului în care există una existentă care conține ambii utilizatori (deoarece nu dorim să avem mai multe instanțe ale aceleiași perechi de utilizatori), caz în care returnează camera de chat existentă.
Cu toate acestea, datorită designului Firestore, în prezent nu acceptă interogări imbricate array-contains
. Deci nu putem prelua fluxul de date adecvat, dar trebuie să efectuăm filtrare suplimentară din partea noastră. Această soluție constă în preluarea tuturor camerelor de chat pentru utilizatorul conectat și apoi în căutarea celei care conține și utilizatorul selectat:
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 asemenea, din cauza constrângerilor de proiectare similare, Firebase nu acceptă în prezent actualizări ale matricei (inserarea unui element nou într-o valoare a câmpului matricei existentă) cu o valoare specială FieldValue.serverTimestamp()
.
Această valoare indică platformei că câmpul care conține aceasta în loc de o valoare reală ar trebui completat cu marcajul de timp real pe server în momentul în care are loc tranzacția. În schimb, folosim DateTime.now()
în momentul în care creăm noul nostru obiect serializat cu mesaje și inserăm acel obiect în colecția de mesaje din camera de chat.
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; } }
Încheierea
Evident, aplicația de mesagerie Flutter pe care am dezvoltat-o este mai mult o dovadă de concept decât o aplicație de mesagerie instantanee pregătită pentru piață. Ca idei de dezvoltare ulterioară, s-ar putea lua în considerare introducerea de criptare end-to-end sau de conținut bogat (chat-uri de grup, atașamente media, analiza URL). Dar înainte de toate acestea, ar trebui să implementăm notificările push, deoarece acestea sunt aproape o caracteristică obligatorie pentru o aplicație de mesagerie instantanee și am scos-o din sfera acestui articol de dragul conciziei. În plus, Firestore îi lipsesc încă câteva funcții pentru a avea interogări mai simple și mai precise, asemănătoare array-contains
interogări.
După cum sa menționat la începutul articolului, Flutter s-a maturizat doar recent într-o ediție stabilă 1.0 și va continua să crească, nu numai când vine vorba de caracteristicile și capabilitățile cadru, ci și când vine vorba de comunitatea de dezvoltare și bibliotecile terțe și resurse. Este logic să vă investiți timpul în a vă familiariza cu dezvoltarea aplicației Flutter acum, deoarece este în mod clar aici pentru a rămâne și pentru a vă accelera procesul de dezvoltare pentru mobil.
Fără nicio cheltuială suplimentară, dezvoltatorii Flutter vor fi, de asemenea, pregătiți să vizeze sistemul de operare Google în curs de dezvoltare – Fuchsia.