Tutorial Flutter: come creare la tua prima app Flutter
Pubblicato: 2022-03-11Cos'è Flutter?
Flutter è l'SDK per lo sviluppo di app mobili di Google che consente al tuo prodotto di indirizzare contemporaneamente entrambe le piattaforme Android e iOS, senza la necessità di mantenere due basi di codice separate. Inoltre, le app che utilizzano Flutter possono anche essere compilate per indirizzare il prossimo sistema operativo Fuchsia di Google.
Flutter ha recentemente raggiunto un importante traguardo: la versione stabile 1.0. L'uscita è avvenuta a Londra, il 5 dicembre 2018, all'evento Flutter Live. Sebbene possa ancora essere considerata un'impresa software precoce e in evoluzione, questo articolo si concentrerà su un concetto già collaudato e dimostrerà come sviluppare un'app di messaggistica completamente funzionale che si rivolge a entrambe le principali piattaforme mobili utilizzando Flutter 1.2 e Firebase.
Come si può vedere dal grafico sottostante, Flutter ha guadagnato molti utenti negli ultimi mesi. Nel 2018, la quota di mercato di Flutter è raddoppiata ed è sulla buona strada per superare React Native in termini di query di ricerca, da qui la nostra decisione di creare un nuovo tutorial di Flutter.
Nota: questo articolo si concentra solo su alcuni bit dell'implementazione. Il riferimento completo al codice sorgente per il progetto è disponibile in questo repository GitHub.
Prerequisiti
Anche se è stato fatto uno sforzo per consentire ai lettori di seguire e realizzare questo progetto anche se è il loro primo tentativo di sviluppo mobile, molti concetti fondamentali di sviluppo mobile che non sono specifici di Flutter vengono menzionati e utilizzati senza una spiegazione dettagliata.
Ciò è stato intrapreso per brevità dell'articolo poiché uno dei suoi obiettivi è che il lettore completi il progetto in una sola seduta. Infine, l'articolo presuppone che tu abbia già configurato il tuo ambiente di sviluppo, inclusi i plug-in Android Studio richiesti e Flutter SDK.
Configurazione Firebase
La configurazione di Firebase è l'unica cosa che dobbiamo fare in modo indipendente per ciascuna piattaforma. Prima di tutto, assicurati di creare un nuovo progetto nella dashboard di Firebase e di aggiungere applicazioni Android e iOS nell'area di lavoro appena generata. La piattaforma produrrà due file di configurazione che devi scaricare: google-services.json
per Android e GoogleService-Info.plist
per iOS. Prima di chiudere la dashboard, assicurati di abilitare i provider di autenticazione Firebase e Google poiché li utilizzeremo per l'identificazione degli utenti. Per fare ciò, scegli la voce Autenticazione dal menu e quindi seleziona la scheda Metodo di accesso.
Ora puoi chiudere la dashboard mentre il resto della configurazione avviene nella nostra codebase. Prima di tutto, dobbiamo inserire i file che abbiamo scaricato nel nostro progetto. Il file google-services.json
deve essere posizionato nella cartella $(FLUTTER_PROJECT_ROOT)/android/app
e GoogleService-Info.plist
deve essere posizionato nella $(FLUTTER_PROJECT_ROOT)/ios/Runner
. Successivamente, dobbiamo effettivamente configurare le librerie Firebase che utilizzeremo nel progetto e collegarle ai file di configurazione. Questo viene fatto specificando i pacchetti Dart (librerie) che useremo nel file pubspec.yaml
del nostro progetto. Nella sezione delle dipendenze del file, incolla il seguente snippet:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
I primi due non sono correlati a Firebase ma verranno usati frequentemente nel progetto. Gli ultimi due sono, si spera, autoesplicativi.
Infine, dobbiamo configurare le impostazioni del progetto specifiche della piattaforma che consentiranno il completamento del nostro flusso di autenticazione. Sul lato Android, dobbiamo aggiungere il plug-in Gradle dei servizi Google alla nostra configurazione Gradle a livello di progetto. In altre parole, dobbiamo aggiungere il seguente elemento all'elenco delle dipendenze nel file $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
Quindi dobbiamo applicare quel plugin aggiungendo questa riga alla fine di $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
L'ultima cosa per questa piattaforma è arruolare i parametri dell'applicazione Facebook. Quello che stiamo cercando qui è la modifica di questi due file: $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml
e $(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>
Ora è il momento di iOS. Fortunatamente, in questo caso abbiamo solo bisogno di cambiare un file. Aggiungi i seguenti valori (nota che l'elemento CFBundleURLTypes
potrebbe già esistere nell'elenco; in tal caso, è necessario aggiungere questi elementi all'array esistente invece di dichiararlo nuovamente) a $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
file:
<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>
Una parola sull'architettura BLoC
Questo standard di architettura è stato descritto in uno dei nostri articoli precedenti, dimostrando l'uso di BLoC per la condivisione del codice in Flutter e AngularDart, quindi non lo spiegheremo in dettaglio qui.
L'idea di base alla base dell'idea principale è che ogni schermata ha le seguenti classi: - view - che è responsabile della visualizzazione dello stato corrente e della delega dell'input dell'utente come eventi da bloccare. - stato - che rappresenta i dati "in tempo reale" con cui l'utente interagisce utilizzando la vista corrente. - bloc - che risponde agli eventi e aggiorna lo stato di conseguenza, richiedendo facoltativamente i dati da uno o più repository locali o remoti. - evento - che è il risultato di un'azione definita che può o meno modificare lo stato attuale.
Come rappresentazione grafica, può essere pensata in questo modo:
Inoltre, abbiamo una directory del modello che contiene classi di dati e repository che producono istanze di queste classi.
Sviluppo dell'interfaccia utente
La creazione dell'interfaccia utente tramite Flutter viene eseguita completamente in Dart, a differenza dello sviluppo di app native in Android e iOS in cui l'interfaccia utente viene creata utilizzando lo schema XML ed è completamente separata dalla base di codice della logica aziendale. Utilizzeremo composizioni di elementi dell'interfaccia utente relativamente semplici con diversi componenti in base allo stato corrente (es. isLoading, isEmpty parametri). L'interfaccia utente in Flutter ruota attorno ai widget, o meglio all'albero dei widget. I widget possono essere stateless o stateful. Quando si tratta di quelli con stato, è importante sottolineare che, quando setState()
viene chiamato su un particolare widget che è attualmente visualizzato (chiamandolo nel costruttore o dopo che è stato eliminato si verifica un errore di runtime), un build and draw pass è programmato per essere eseguito al prossimo ciclo di imbutitura.
Per brevità, mostreremo solo una delle classi UI (view) qui:
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); } }
Il resto delle classi dell'interfaccia utente segue gli stessi schemi ma forse ha azioni diverse e potrebbe presentare un albero di widget di stato vuoto oltre allo stato di caricamento.

Autenticazione
Come avrai intuito, utilizzeremo le librerie google_sign_in
e flutter_facebook_login
per autenticare l'utente facendo affidamento sul suo profilo di social network. Prima di tutto, assicurati di importare questi pacchetti nel file che gestirà la logica della richiesta di accesso:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
Ora avremo due parti indipendenti che si occuperanno del nostro flusso di autenticazione. Il primo avvierà una richiesta di accesso a Facebook o 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)); } }
Il secondo verrà chiamato quando avremo i dati del profilo da uno dei due provider. A tal fine, istruiremo il nostro gestore di accesso ad ascoltare firebase_auth onAuthStateChange
stream:
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'implementazione di UserRepo
e LoginRepo
non verrà pubblicata qui, ma sentiti libero di dare un'occhiata al repository GitHub per un riferimento completo.
Tutorial Flutter: come creare un'app di messaggistica istantanea
Infine, arriviamo alla parte interessante. Come suggerisce il nome, i messaggi dovrebbero essere scambiati il più velocemente possibile, idealmente dovrebbe essere istantaneo . Fortunatamente, cloud_firestore
ci consente di interagire con l'istanza Firestore e possiamo utilizzare la sua funzione snapshots()
per aprire un flusso di dati che ci fornirà aggiornamenti in tempo reale. A mio parere, tutto il codice chat_repo
è piuttosto semplice con l'eccezione del metodo startChatroomForUsers
. È responsabile della creazione di una nuova chat room per due utenti a meno che non ce ne sia una esistente che contenga entrambi gli utenti (poiché non vogliamo avere più istanze della stessa coppia di utenti), nel qual caso restituisce la chat room esistente.
Tuttavia, a causa del design di Firestore, attualmente non supporta le query nidificate array-contains
. Quindi non possiamo recuperare il flusso di dati appropriato ma dobbiamo eseguire filtri aggiuntivi dalla nostra parte. Tale soluzione consiste nel recuperare tutte le chatroom per l'utente che ha effettuato l'accesso e quindi cercare quella che contiene anche l'utente selezionato:
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); } }
Inoltre, a causa di vincoli di progettazione simili, Firebase attualmente non supporta gli aggiornamenti dell'array (inserimento di un nuovo elemento in un valore di campo dell'array esistente) con un valore FieldValue.serverTimestamp()
speciale.
Questo valore indica alla piattaforma che il campo che contiene questo invece di un valore effettivo deve essere popolato con il timestamp effettivo sul server nel momento in cui avviene la transazione. Invece, stiamo usando DateTime.now()
nel momento in cui stiamo creando il nostro nuovo oggetto serializzato di messaggi e inserendo quell'oggetto nella raccolta di messaggi della chat room.
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; } }
Avvolgendo
Ovviamente, l'app di messaggistica Flutter che abbiamo sviluppato è più una prova di concetto che un'applicazione di messaggistica istantanea pronta per il mercato. Come idee per un ulteriore sviluppo, si potrebbe prendere in considerazione l'introduzione della crittografia end-to-end o del contenuto avanzato (chat di gruppo, allegati multimediali, analisi degli URL). Ma prima di tutto, si dovrebbero implementare le notifiche push in quanto sono praticamente una funzionalità indispensabile per un'applicazione di messaggistica istantanea e l'abbiamo spostata fuori dall'ambito di questo articolo per motivi di brevità. Inoltre, Firestore manca ancora di un paio di funzionalità per avere query array-contains
nidificati simili a dati più semplici e più precisi.
Come accennato all'inizio dell'articolo, Flutter è maturato solo di recente nella versione stabile 1.0 e continuerà a crescere, non solo quando si tratta di funzionalità e funzionalità del framework, ma anche per quanto riguarda la comunità di sviluppo e le librerie di terze parti e risorse. Ha senso investire ora il tuo tempo per familiarizzare con lo sviluppo dell'app Flutter, poiché è chiaramente qui per restare e accelerare il processo di sviluppo per dispositivi mobili.
Senza spese aggiuntive, gli sviluppatori Flutter saranno anche pronti a prendere di mira l'emergente OS di Google: il fucsia.