Samouczek Flutter: Jak stworzyć swoją pierwszą aplikację Flutter
Opublikowany: 2022-03-11Co to jest trzepotanie?
Flutter to pakiet SDK do tworzenia aplikacji mobilnych Google, który umożliwia jednoczesne kierowanie produktu na platformy Android i iOS, bez konieczności utrzymywania dwóch oddzielnych baz kodu. Co więcej, aplikacje korzystające z Fluttera mogą być również kompilowane pod kątem nadchodzącego systemu operacyjnego Google Fuchsia.
Flutter ostatnio osiągnął kamień milowy - stabilną wersję 1.0. Wydanie miało miejsce w Londynie, 5 grudnia 2018 roku, podczas wydarzenia Flutter Live. Chociaż nadal można to uznać za wczesne i rozwijające się przedsięwzięcie programistyczne, ten artykuł skupi się na sprawdzonej już koncepcji i pokaże, jak opracować w pełni funkcjonalną aplikację do przesyłania wiadomości, która jest przeznaczona dla obu głównych platform mobilnych przy użyciu Flutter 1.2 i Firebase.
Jak widać na poniższym wykresie, Flutter zdobywał w ostatnich miesiącach wielu użytkowników. W 2018 r. udział Fluttera w rynku podwoił się i jest na dobrej drodze, by przewyższyć React Native pod względem zapytań wyszukiwania, stąd nasza decyzja o utworzeniu nowego samouczka Fluttera.
Uwaga: ten artykuł skupia się tylko na niektórych fragmentach implementacji. Pełne odniesienie do kodu źródłowego projektu można znaleźć w tym repozytorium GitHub.
Warunki wstępne
Mimo że podjęto wysiłki, aby umożliwić czytelnikom śledzenie i realizację tego projektu, nawet jeśli jest to ich pierwsza próba rozwoju mobilnego, wiele podstawowych koncepcji rozwoju mobilnego, które nie są specyficzne dla Fluttera, jest wymienianych i używanych bez szczegółowego wyjaśnienia.
Zostało to podjęte dla zwięzłości artykułu, ponieważ jednym z jego celów jest ukończenie projektu przez czytelnika w jednym posiedzeniu. Wreszcie, artykuł zakłada, że masz już skonfigurowane środowisko programistyczne, w tym wymagane wtyczki Android Studio i Flutter SDK.
Konfiguracja Firebase
Konfiguracja Firebase to jedyna rzecz, którą musimy zrobić niezależnie dla każdej platformy. Przede wszystkim upewnij się, że tworzysz nowy projekt w Firebase Dashboard i dodajesz aplikacje na Androida i iOS w nowo wygenerowanym obszarze roboczym. Platforma wygeneruje dwa pliki konfiguracyjne, które musisz pobrać: google-services.json
dla Androida i GoogleService-Info.plist
dla iOS. Przed zamknięciem panelu upewnij się, że włączyłeś dostawców uwierzytelniania Firebase i Google, ponieważ będziemy ich używać do identyfikacji użytkownika. Aby to zrobić, wybierz z menu element Uwierzytelnianie, a następnie wybierz kartę Metoda logowania.
Teraz możesz zamknąć pulpit nawigacyjny, ponieważ reszta konfiguracji odbywa się w naszej bazie kodu. Przede wszystkim musimy umieścić pobrane pliki w naszym projekcie. Plik google-services.json
należy umieścić w folderze $(FLUTTER_PROJECT_ROOT)/android/app
, a GoogleService-Info.plist
należy umieścić w katalogu $(FLUTTER_PROJECT_ROOT)/ios/Runner
. Następnie musimy właściwie skonfigurować biblioteki Firebase, których będziemy używać w projekcie, i połączyć je z plikami konfiguracyjnymi. Odbywa się to poprzez określenie pakietów Dart (bibliotek), których będziemy używać w pliku pubspec.yaml
naszego projektu. W sekcji zależności pliku wklej następujący fragment:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
Pierwsze dwa nie są związane z Firebase, ale będą często używane w projekcie. Mam nadzieję, że dwa ostatnie nie wymagają wyjaśnień.
Na koniec musimy skonfigurować ustawienia projektu specyficzne dla platformy, które umożliwią pomyślne zakończenie procesu uwierzytelniania. Po stronie Androida musimy dodać wtyczkę Gradle google-services do naszej konfiguracji Gradle na poziomie projektu. Innymi słowy, musimy dodać następujący element do listy zależności w pliku $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
Następnie musimy zastosować tę wtyczkę, dodając tę linię na końcu $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
Ostatnią rzeczą dla tej platformy jest zapisanie parametrów aplikacji Facebooka. Tutaj szukamy edycji tych dwóch plików - $(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>
Teraz czas na iOS. Na szczęście w tym przypadku wystarczy zmienić tylko jeden plik. Dodaj następujące wartości (zauważ, że element CFBundleURLTypes
może już istnieć na liście; w takim przypadku musisz dodać te elementy do istniejącej tablicy zamiast deklarować ją ponownie) do $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
plik:
<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>
Słowo o architekturze BLoC
Ten standard architektury został opisany w jednym z naszych poprzednich artykułów, demonstrując użycie BLoC do współdzielenia kodu w Flutter i AngularDart, więc nie będziemy go tutaj szczegółowo wyjaśniać.
Podstawową ideą stojącą za główną ideą jest to, że każdy ekran ma następujące klasy: - view - odpowiada za wyświetlanie aktualnego stanu i delegowanie danych wejściowych użytkownika jako zdarzeń do bloku. - stan - reprezentujący „na żywo” dane, z którymi użytkownik wchodzi w interakcję przy użyciu bieżącego widoku. - bloc - który reaguje na zdarzenia i odpowiednio aktualizuje stan, opcjonalnie żądając danych z jednego lub wielu lokalnych lub zdalnych repozytoriów. - zdarzenie - czyli określony wynik akcji, który może, ale nie musi, zmienić aktualny stan.
Jako graficzną reprezentację można to sobie wyobrazić w następujący sposób:
Dodatkowo mamy katalog modeli , który zawiera klasy danych i repozytoria, które produkują instancje tych klas.
Rozwój interfejsu użytkownika
Tworzenie interfejsu użytkownika za pomocą Fluttera odbywa się całkowicie w Dart, w przeciwieństwie do tworzenia aplikacji natywnych w systemach Android i iOS, gdzie interfejs użytkownika jest budowany przy użyciu schematu XML i jest całkowicie oddzielony od bazy kodu logiki biznesowej. Będziemy używać stosunkowo prostych kompozycji elementów interfejsu użytkownika z różnymi komponentami w oparciu o aktualny stan (np. parametry isLoading, isEmpty). Interfejs użytkownika we Flutterze obraca się wokół widżetów, a raczej drzewa widżetów. Widgety mogą być bezstanowe lub stanowe. Jeśli chodzi o stanowe, należy podkreślić, że gdy setState()
jest wywoływana na konkretnym widżecie, który jest aktualnie wyświetlany (wywołanie go w konstruktorze lub po jego usunięciu skutkuje błędem w czasie wykonywania), następuje zaplanowane do wykonania w następnym cyklu rysowania.
Dla zwięzłości pokażemy tutaj tylko jedną z klas UI (widoku):
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); } }
Pozostałe klasy interfejsu użytkownika mają te same wzorce, ale być może mają różne akcje i mogą zawierać puste drzewo widżetów stanu oprócz stanu ładowania.

Uwierzytelnianie
Jak można się domyślić, będziemy używać bibliotek google_sign_in
i flutter_facebook_login
do uwierzytelniania użytkownika na podstawie jego profilu w sieci społecznościowej. Przede wszystkim zaimportuj te pakiety do pliku, który będzie obsługiwał logikę żądania logowania:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
Teraz będziemy mieć dwie niezależne części, które zajmą się naszym przepływem uwierzytelniania. Pierwszy z nich zainicjuje żądanie logowania do Facebooka lub 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)); } }
Drugi zostanie wywołany, gdy otrzymamy dane profilu od dowolnego dostawcy. Zrobimy to, instruując nasz program obsługi logowania, aby nasłuchiwał strumienia 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)); }); } }
UserRepo
i LoginRepo
nie zostanie tutaj opublikowana, ale zachęcamy do zapoznania się z repozytorium GitHub w celu uzyskania pełnego odniesienia.
Flutter Tutorial: Jak zbudować aplikację do obsługi wiadomości błyskawicznych
Wreszcie dochodzimy do interesującej części. Jak sama nazwa wskazuje, wiadomości powinny być wymieniane tak szybko, jak to możliwe, najlepiej natychmiast . Na szczęście cloud_firestore
pozwala nam na interakcję z instancją Firestore i możemy użyć jej funkcji snapshots()
, aby otworzyć strumień danych, który dostarczy nam aktualizacje w czasie rzeczywistym. Moim zdaniem cały kod chat_repo
jest całkiem prosty, z wyjątkiem metody startChatroomForUsers
. Jest odpowiedzialny za utworzenie nowego pokoju rozmów dla dwóch użytkowników, chyba że istnieje już istniejący pokój, który zawiera obu użytkowników (ponieważ nie chcemy mieć wielu wystąpień tej samej pary użytkowników), w którym to przypadku zwraca istniejący pokój rozmów.
Jednak ze względu na projekt Firestore obecnie nie obsługuje zagnieżdżonych zapytań array-contains
. Nie możemy więc pobrać odpowiedniego strumienia danych, ale musimy wykonać dodatkowe filtrowanie po naszej stronie. Rozwiązanie to polega na pobraniu wszystkich czatów dla zalogowanego użytkownika, a następnie wyszukaniu tego, który zawiera również wybranego użytkownika:
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); } }
Ponadto ze względu na podobne ograniczenia projektowe Firebase obecnie nie obsługuje aktualizacji tablicy (wstawiania nowego elementu do istniejącej wartości pola tablicy) za pomocą specjalnej wartości FieldValue.serverTimestamp()
.
Ta wartość wskazuje platformie, że pole zawierające tę wartość zamiast rzeczywistej wartości powinno zostać wypełnione faktycznym znacznikiem czasu na serwerze w momencie przeprowadzania transakcji. Zamiast tego używamy DateTime.now()
w momencie tworzenia nowego obiektu serializowanego wiadomości i wstawiania go do kolekcji wiadomości pokoju rozmów.
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; } }
Zawijanie
Oczywiście opracowana przez nas aplikacja do przesyłania wiadomości Flutter jest bardziej weryfikacją koncepcji niż gotową na rynek aplikacją do obsługi wiadomości błyskawicznych. Jako pomysły na dalszy rozwój można rozważyć wprowadzenie szyfrowania end-to-end lub bogatej zawartości (czaty grupowe, załączniki multimedialne, parsowanie adresów URL). Ale przed tym wszystkim należy zaimplementować powiadomienia wypychane, ponieważ są one praktycznie niezbędną funkcją aplikacji do obsługi wiadomości błyskawicznych i przenieśliśmy je z zakresu tego artykułu ze względu na zwięzłość. Ponadto Firestore wciąż brakuje kilku funkcji, aby mieć prostsze i dokładniejsze zapytania zagnieżdżone w postaci array-contains
dane.
Jak wspomniano na początku artykułu, Flutter dopiero niedawno dojrzał do stabilnej wersji 1.0 i będzie się rozwijał, nie tylko jeśli chodzi o funkcje i możliwości frameworka, ale także o społeczność programistów i biblioteki innych firm oraz Surowce. Warto zainwestować swój czas w zapoznanie się z rozwojem aplikacji Flutter już teraz, ponieważ jest to oczywiste, aby pozostać i przyspieszyć proces tworzenia aplikacji mobilnych.
Bez dodatkowych kosztów programiści Flutter będą również gotowi na atakowanie powstającego systemu operacyjnego Google – Fuksja.