1. Zanim zaczniesz
W ramach tego ćwiczenia w programie poznasz podstawy Firebase do tworzenia aplikacji mobilnych Flutter na Androida i iOS.
Wymagania wstępne
- znajomość Fluttera,
- Pakiet Flutter SDK
- Edytor tekstu do wyboru
Czego się nauczysz
- Jak za pomocą Fluttera utworzyć aplikację do potwierdzania uczestnictwa w wydarzeniu i prowadzenia czatu z gościem na Androida, iOS, w internecie i na macOS.
- Jak uwierzytelniać użytkowników za pomocą usługi Uwierzytelnianie Firebase i synchronizować dane z Firestore.
Czego potrzebujesz
Dowolne z tych urządzeń:
- Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty.
- Symulator iOS (wymaga narzędzi Xcode).
- Emulator Androida (wymaga konfiguracji w Android Studio).
Potrzebujesz też:
- wybraną przeglądarkę, na przykład Google Chrome;
- IDE lub edytor tekstu skonfigurowany za pomocą wtyczek Dart i Flutter, na przykład Android Studio lub Visual Studio Code.
- najnowszą wersję
stable
Flutter lubbeta
, jeśli lubisz nowinki. - Konto Google do tworzenia projektu Firebase i zarządzania nim.
Firebase
CLI zalogowana na Twoje konto Google.
2. Pobieranie przykładowego kodu
Pobierz początkową wersję projektu z GitHuba:
- Korzystając z wiersza poleceń, skopiuj repozytorium GitHub z katalogu
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Katalog flutter-codelabs
zawiera kod kolekcji zajęć z kodem. Kod tego CodeLab znajduje się w katalogu flutter-codelabs/firebase-get-to-know-flutter
. Katalog zawiera serię zrzutów, które pokazują, jak powinien wyglądać projekt pod koniec każdego kroku. Na przykład jesteś na 2. kroku.
- Znajdź pliki pasujące do drugiego kroku:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
Jeśli chcesz przejść do następnego kroku lub sprawdzić, jak coś powinno wyglądać po wykonaniu danego kroku, otwórz katalog o nazwie tego kroku.
Zaimportuj aplikację startową
- Otwórz lub zaimportuj katalog
flutter-codelabs/firebase-get-to-know-flutter/step_02
w preferowanym IDE. Ten katalog zawiera kod startowy ćwiczenia z programowania, które obejmuje niedziałającą jeszcze aplikację Flutter Meetup.
Znajdź pliki, które wymagają dopracowania
Kod w tej aplikacji jest rozproszony po wielu katalogach. Taki podział funkcji ułatwia pracę, ponieważ kod jest grupowany według funkcji.
- Znajdź następujące pliki:
lib/main.dart
: ten plik zawiera główny punkt wejścia i widżet aplikacji.lib/home_page.dart
: ten plik zawiera widżet strony głównej.lib/src/widgets.dart
: ten plik zawiera kilka widżetów, które pomagają ujednolicić styl aplikacji. Składają się na ekran aplikacji startowej.lib/src/authentication.dart
: ten plik zawiera częściową implementację uwierzytelniania z zestawem widżetów umożliwiających logowanie się w ramach uwierzytelniania Firebase za pomocą adresu e-mail. Te widżety do obsługi uwierzytelniania nie są jeszcze używane w aplikacji startowej, ale wkrótce je dodasz.
Dodaj dodatkowe pliki, które są wymagane do utworzenia pozostałej części aplikacji.
Sprawdź plik lib/main.dart
Ta aplikacja korzysta z pakietu google_fonts
, aby ustawić Roboto jako domyślną czcionkę w całej aplikacji. Możesz przeglądać fonts.google.com i używać tych czcionek w różnych częściach aplikacji.
Korzystasz z widżetów pomocniczych z pliku lib/src/widgets.dart
w postaci Header
, Paragraph
i IconAndDetail
. Te widżety eliminują zduplikowany kod, aby zwiększyć przejrzystość strony w układzie strony opisanym w sekcji HomePage
. Pozwala to również uzyskać spójny wygląd.
Oto jak wygląda Twoja aplikacja na Androida, iOS, w internecie i na macOS:
3. Tworzenie i konfigurowanie projektu Firebase
Wyświetlanie informacji o wydarzeniu jest przydatne dla gości, ale nie jest zbyt przydatne dla nikogo innego. Musisz dodać do aplikacji niektóre funkcje dynamiczne. Aby to zrobić, musisz połączyć Firebase z aplikacją. Aby zacząć korzystać z Firebase, musisz utworzyć i skonfigurować projekt Firebase.
Tworzenie projektu Firebase
- Zaloguj się w Firebase.
- W konsoli kliknij Dodaj projekt lub Utwórz projekt.
- W polu Nazwa projektu wpisz Firebase-Flutter-Codelab, a potem kliknij Dalej.
- Klikaj opcje tworzenia projektów. Jeśli pojawi się taka prośba, zaakceptuj warunki korzystania z Firebase, ale pomiń konfigurację Google Analytics, ponieważ nie będziesz z niego korzystać w przypadku tej aplikacji.
Więcej informacji o projektach Firebase znajdziesz w artykule Informacje o projektach Firebase.
Aplikacja korzysta z tych usług Firebase, które są dostępne dla aplikacji internetowych:
- Uwierzytelnianie: pozwala użytkownikom logować się w aplikacji.
- Firestore: zapisuje uporządkowane dane w chmurze i otrzymuje natychmiastowe powiadomienia o zmianie danych.
- Reguły zabezpieczeń Firebase: zabezpiecza bazę danych.
Niektóre z tych usług wymagają specjalnej konfiguracji lub ich włączenia w konsoli Firebase.
Włączanie uwierzytelniania przy logowaniu za pomocą adresu e-mail
- W panelu Omówienie projektu w konsoli Firebase rozwiń menu Tworzenie.
- Kliknij Uwierzytelnianie > Pierwsze kroki > Metoda logowania > E-mail/hasło > Włącz > Zapisz.
Konfigurowanie Firestore
Aplikacja internetowa używa Firestore do zapisywania wiadomości czatu i odbierania nowych wiadomości.
Aby skonfigurować Firestore w projekcie Firebase:
- W panelu po lewej stronie w konsoli Firebase rozwiń Kompilacja, a potem wybierz Baza danych Firestore.
- Kliknij Utwórz bazę danych.
- W polu Identyfikator bazy danych pozostaw wartość
(default)
. - Wybierz lokalizację bazy danych i kliknij Dalej.
W przypadku prawdziwej aplikacji musisz wybrać lokalizację znajdującą się w pobliżu użytkowników. - Kliknij Rozpocznij w trybie testowym. Przeczytaj wyłączenie odpowiedzialności dotyczące reguł bezpieczeństwa.
W dalszej części tego Codelab dodasz reguły bezpieczeństwa, aby chronić swoje dane. Nie udostępniaj ani nie udostępniaj publicznie aplikacji bez dodania reguł bezpieczeństwa dla bazy danych. - Kliknij Utwórz.
4. Skonfiguruj Firebase
Aby korzystać z Firebase razem z Flutter, musisz wykonać te zadania, aby skonfigurować projekt Flutter tak, aby prawidłowo korzystał z bibliotek FlutterFire
:
- Dodaj do projektu zależności
FlutterFire
. - Zarejestruj odpowiednią platformę w projekcie Firebase.
- Pobierz plik konfiguracji dla danej platformy, a potem dodaj go do kodu.
W katalogu najwyższego poziomu aplikacji Flutter znajdują się podkatalogi android
, ios
, macos
i web
, w których znajdują się odpowiednio pliki konfiguracji platformy na iOS i Androida.
Skonfiguruj zależności
Musisz dodać biblioteki FlutterFire
dla 2 usług Firebase, których używasz w tej aplikacji: uwierzytelniania i Firestore.
- W wierszu poleceń dodaj te zależności:
$ flutter pub add firebase_core
Pakiet firebase_core
to wspólny kod wymagany we wszystkich wtyczkach Firebase Flutter.
$ flutter pub add firebase_auth
Pakiet firebase_auth
umożliwia integrację z Uwierzytelnianiem.
$ flutter pub add cloud_firestore
Pakiet cloud_firestore
zapewnia dostęp do miejsca na dane Firestore.
$ flutter pub add provider
Pakiet firebase_ui_auth
zawiera zestaw widżetów i narzędzi, które przyspieszają pracę programistów dzięki procesom uwierzytelniania.
$ flutter pub add firebase_ui_auth
Dodasz wymagane pakiety, ale aby prawidłowo korzystać z Firebase, musisz też skonfigurować projekty Web Runner, iOS, macOS i Android. Używasz też pakietu provider
, który umożliwia rozdzielenie logiki biznesowej od logiki wyświetlania.
Instalowanie interfejsu wiersza poleceń FlutterFire
Interfejs wiersza poleceń FlutterFire zależy od podstawowego interfejsu wiersza poleceń Firebase.
- Jeśli interfejs wiersza poleceń Firebase nie jest jeszcze zainstalowany na Twoim komputerze, zainstaluj go na swoim komputerze.
- Zainstaluj interfejs wiersza poleceń FlutterFire:
$ dart pub global activate flutterfire_cli
Po zainstalowaniu polecenie flutterfire
jest dostępne na całym świecie.
Konfigurowanie aplikacji
Interfejs wiersza poleceń wyodrębnia informacje z projektu Firebase i wybranych aplikacji projektów, aby wygenerować całą konfigurację dla konkretnej platformy.
W katalogu głównym aplikacji uruchom polecenie configure
:
$ flutterfire configure
Polecenie konfiguracji przeprowadzi Cię przez te procesy:
- Wybierz projekt Firebase na podstawie pliku
.firebaserc
lub z poziomu konsoli Firebase. - Określ platformy konfiguracyjne, takie jak Android, iOS, macOS i internet.
- Określ aplikacje Firebase, z których chcesz wyodrębnić konfigurację. Domyślnie interfejs wiersza poleceń próbuje automatycznie dopasować aplikacje Firebase na podstawie bieżącej konfiguracji projektu.
- Wygeneruj plik
firebase_options.dart
w projekcie.
Konfigurowanie systemu macOS
Flutter w systemie macOS umożliwia tworzenie aplikacji w pełni położonych w piaskownicy. Ponieważ aplikacja integruje się z siecią, aby komunikować się z serwerami Firebase, musisz skonfigurować ją z przywilejami klienta sieci.
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Więcej informacji znajdziesz w artykule Obsługa technologii Flutter na komputerach.
5. Dodawanie funkcji odpowiedzi
Po dodaniu Firebase do aplikacji możesz utworzyć przycisk Odpowiedz, który rejestruje osoby korzystające z uwierzytelniania. W przypadku natywnego Androida, natywnego iOS i WWW dostępne są wstępnie utworzone pakiety FirebaseUI Auth
, ale w przypadku Fluttera musisz samodzielnie utworzyć tę funkcję.
Pobrany wcześniej projekt zawierał zestaw widżetów, które implementują interfejs użytkownika przez większość procesu uwierzytelniania. Wdrażasz logikę biznesową, aby zintegrować Uwierzytelnianie z aplikacją.
Dodawanie logiki biznesowej za pomocą pakietu Provider
Użyj pakietu provider
, aby udostępnić scentralizowany obiekt stanu aplikacji w całym drzewie widżetów Flutter aplikacji:
- Utwórz nowy plik o nazwie
app_state.dart
z tą zawartością:
lib/app_state.dart
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
class ApplicationState extends ChangeNotifier {
ApplicationState() {
init();
}
bool _loggedIn = false;
bool get loggedIn => _loggedIn;
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
} else {
_loggedIn = false;
}
notifyListeners();
});
}
}
Instrukcje import
wprowadzają Firebase Core i Auth, wczytują pakiet provider
, który udostępnia obiekt stanu aplikacji w całym drzewie widżetów, oraz zawierają widżety uwierzytelniania z pakietu firebase_ui_auth
.
Ten obiekt stanu aplikacji ApplicationState
ma 1 główną odpowiedzialność na tym etapie, która polega na powiadomieniu drzewa widżetów o aktualizacji stanu uwierzytelniania.
Korzystasz z usług dostawcy tylko do informowania aplikacji o stanie logowania użytkownika. Aby umożliwić użytkownikowi zalogowanie się, korzystasz z interfejsów dostępnych w pakiecie firebase_ui_auth
, które są świetnym sposobem na szybkie wczytywanie ekranów logowania w aplikacjach.
Integracja procesu uwierzytelniania
- Zmień importy u góry pliku
lib/main.dart
:
lib/main.dart
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'home_page.dart';
- Połącz stan aplikacji z inicjalizacją aplikacji, a potem dodaj proces uwierzytelniania do
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
Modyfikacja funkcji main()
sprawia, że pakiet dostawcy odpowiada za utworzenie instancji obiektu stanu aplikacji za pomocą widżetu ChangeNotifierProvider
. Użyjesz tej konkretnej klasy provider
, ponieważ obiekt stanu aplikacji rozszerza klasę ChangeNotifier
, która informuje pakiet provider
o tym, kiedy ponownie wyświetlić zależne widżety.
- Zaktualizuj aplikację tak, aby obsługiwała nawigację na różnych ekranach dostępnych w FirebaseUI. W tym celu utwórz konfigurację
GoRouter
:
lib/main.dart
// Add GoRouter configuration outside the App class
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'sign-in',
builder: (context, state) {
return SignInScreen(
actions: [
ForgotPasswordAction(((context, email) {
final uri = Uri(
path: '/sign-in/forgot-password',
queryParameters: <String, String?>{
'email': email,
},
);
context.push(uri.toString());
})),
AuthStateChangeAction(((context, state) {
final user = switch (state) {
SignedIn state => state.user,
UserCreated state => state.credential.user,
_ => null
};
if (user == null) {
return;
}
if (state is UserCreated) {
user.updateDisplayName(user.email!.split('@')[0]);
}
if (!user.emailVerified) {
user.sendEmailVerification();
const snackBar = SnackBar(
content: Text(
'Please check your email to verify your email address'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
context.pushReplacement('/');
})),
],
);
},
routes: [
GoRoute(
path: 'forgot-password',
builder: (context, state) {
final arguments = state.uri.queryParameters;
return ForgotPasswordScreen(
email: arguments['email'],
headerMaxExtent: 200,
);
},
),
],
),
GoRoute(
path: 'profile',
builder: (context, state) {
return ProfileScreen(
providers: const [],
actions: [
SignedOutAction((context) {
context.pushReplacement('/');
}),
],
);
},
),
],
),
],
);
// end of GoRouter configuration
// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
routerConfig: _router, // new
);
}
}
Z każdym ekranem jest powiązany inny typ działania, zależny od nowego stanu procesu uwierzytelniania. Po większości zmian stanu uwierzytelniania możesz przekierować z powrotem na preferowany ekran, niezależnie od tego, czy jest to ekran główny, czy inny ekran, na przykład profil.
- W metodzie kompilacji klasy
HomePage
zintegruj stan aplikacji z widżetemAuthFunc
:
lib/home_page.dart
import 'package:firebase_auth/firebase_auth.dart' // new
hide EmailAuthProvider, PhoneAuthProvider; // new
import 'package:flutter/material.dart'; // new
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'src/authentication.dart'; // new
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
// Add from here
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
// to here
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
],
),
);
}
}
Tworzysz instancję widżetu AuthFunc
i owijasz go w widżet Consumer
. W przypadku zmiany stanu aplikacji pakiet provider
można zwykle odtworzyć za pomocą widżetu Consumer. Widżet AuthFunc
to dodatkowe widżety, które testujesz.
Testowanie procesu uwierzytelniania
- W aplikacji kliknij przycisk Odpowiedz, aby rozpocząć
SignInScreen
.
- Wpisz adres e-mail. Jeśli jesteś już zarejestrowanym użytkownikiem, system poprosi o podanie hasła. W przeciwnym razie system poprosi Cię o wypełnienie formularza rejestracyjnego.
- Aby sprawdzić proces obsługi błędów, wpisz hasło składające się z mniej niż 6 znaków. Jeśli jesteś zarejestrowany, zobaczysz hasło do tego konta.
- Podaj nieprawidłowe hasła, aby sprawdzić procedurę obsługi błędów.
- Wpisz poprawne hasło. Widzisz widok po zalogowaniu się, który umożliwia użytkownikowi wylogowanie się.
6. Zapisywanie wiadomości w Firestore
To świetnie, że użytkownicy chętnie odwiedzają Twoją aplikację, ale musisz dać im coś innego do zrobienia. Może mogliby zostawiać wiadomości w książce gości? Mogą powiedzieć, dlaczego się cieszą z przyjazdu lub kogo chcą poznać.
Do przechowywania wiadomości czatu, które użytkownicy piszą w aplikacji, możesz używać Firestore.
Model danych
Firestore to baza danych NoSQL, w której dane są dzielone na kolekcje, dokumenty, pola i kolekcje podrzędne. Każda wiadomość z czatu jest przechowywana jako dokument w kolekcji guestbook
, która jest kolekcją najwyższego poziomu.
Dodawanie wiadomości do Firestore
W tej sekcji dodasz funkcję umożliwiającą użytkownikom zapisywanie wiadomości w bazie danych. Najpierw dodaj pole formularza i przycisk wysyłania, a potem kod, który połączy te elementy z bazą danych.
- Utwórz nowy plik o nazwie
guest_book.dart
i dodaj widżet stanowyGuestBook
, aby utworzyć elementy interfejsu pola wiadomości i przycisku wysyłania:
lib/książka_guest.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'src/widgets.dart';
class GuestBook extends StatefulWidget {
const GuestBook({required this.addMessage, super.key});
final FutureOr<void> Function(String message) addMessage;
@override
State<GuestBook> createState() => _GuestBookState();
}
class _GuestBookState extends State<GuestBook> {
final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Leave a message',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter your message to continue';
}
return null;
},
),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await widget.addMessage(_controller.text);
_controller.clear();
}
},
child: Row(
children: const [
Icon(Icons.send),
SizedBox(width: 4),
Text('SEND'),
],
),
),
],
),
),
);
}
}
W tym miejscu znalazło się kilka ciekawych miejsc. Najpierw tworzysz instancję formularza, aby sprawdzić, czy wiadomość zawiera treść, i wyświetlić użytkownikowi komunikat o błędzie, jeśli jej nie ma. Aby sprawdzić formularz, otwierasz jego stan za pomocą funkcji GlobalKey
. Więcej informacji o kluczach i sposobach ich używania znajdziesz w artykule Kiedy używać kluczy.
Pamiętaj też, że widżety są układane w elementy Row
z elementami TextFormField
i StyledButton
, które zawierają element Row
. Pamiętaj też, że element TextFormField
jest zawarty w widżecie Expanded
, co wymusza wypełnienie pola TextFormField
dodatkowego miejsca w wierszu. Aby lepiej zrozumieć, dlaczego jest to wymagane, przeczytaj artykuł Ograniczenia.
Teraz, gdy masz widżet, który umożliwia użytkownikowi wpisanie tekstu do wpisu gościa, musisz go wyświetlić na ekranie.
- Edytuj treść elementu
HomePage
, dodając te 2 wiersze na końcu elementu podrzędnegoListView
:
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),
Taka liczba wystarcza do wyświetlenia widżetu, ale nie wystarczy do zrobienia czegoś przydatnego. Wkrótce zaktualizujesz ten kod, aby działał.
Podgląd aplikacji
Gdy użytkownik kliknie WYŚLIJ, uruchomi się następujący fragment kodu. Powoduje dodanie zawartości pola do wprowadzania wiadomości do kolekcji guestbook
bazy danych. W szczególności metoda addMessageToGuestBook
dodaje treść wiadomości do nowego dokumentu z automatycznie generowanym identyfikatorem w kolekcji guestbook
.
Pamiętaj, że FirebaseAuth.instance.currentUser.uid
to odwołanie do automatycznie wygenerowanego unikalnego identyfikatora, który Authentication udostępnia wszystkim zalogowanym użytkownikom.
- W pliku
lib/app_state.dart
dodaj metodęaddMessageToGuestBook
. Tę funkcję połączysz z interfejsem w następnym kroku.
lib/app_state.dart,
import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
class ApplicationState extends ChangeNotifier {
// Current content of ApplicationState elided ...
// Add from here...
Future<DocumentReference> addMessageToGuestBook(String message) {
if (!_loggedIn) {
throw Exception('Must be logged in');
}
return FirebaseFirestore.instance
.collection('guestbook')
.add(<String, dynamic>{
'text': message,
'timestamp': DateTime.now().millisecondsSinceEpoch,
'name': FirebaseAuth.instance.currentUser!.displayName,
'userId': FirebaseAuth.instance.currentUser!.uid,
});
}
// ...to here.
}
Łączenie interfejsu z bazą danych
Masz interfejs, w którym użytkownik może wpisać tekst, który chce dodać do księgi gości, oraz kod, który umożliwia dodanie wpisu do Firestore. Teraz wystarczy połączyć te 2 elementy.
- W pliku
lib/home_page.dart
wprowadź w widżecieHomePage
te zmiany:
lib/home_page.dart,
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart';
import 'guest_book.dart'; // new
import 'src/authentication.dart';
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Modify from here...
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.loggedIn) ...[
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
),
],
],
),
),
// ...to here.
],
),
);
}
}
2 wiersze dodane na początku tego kroku zostały zastąpione pełną implementacją. Ponownie użyjesz parametru Consumer<ApplicationState>
, by udostępnić stan aplikacji renderowanej części drzewa. Dzięki temu możesz zareagować na osobę, która wpisuje wiadomość w interfejsie i publikuje ją w bazie danych. W następnej sekcji sprawdzisz, czy dodane wiadomości zostały opublikowane w bazie danych.
Testowanie wysyłania wiadomości
- W razie potrzeby zaloguj się w aplikacji.
- Wpisz wiadomość, np.
Hey there!
, a następnie kliknij WYŚLIJ.
To działanie spowoduje zapisanie wiadomości w bazie danych Firestore. Nie widzisz jednak tego komunikatu w aplikacji Flutter, ponieważ musisz jeszcze zaimplementować funkcję pobierania danych, co zrobisz w następnym kroku. Jednak w panelu Baza danych konsoli Firebase możesz zobaczyć dodaną wiadomość w kolekcji guestbook
. Jeśli wyślesz więcej wiadomości, dodasz więcej dokumentów do kolekcji guestbook
. Możesz na przykład zobaczyć następujący fragment kodu:
7. Czytanie wiadomości
To świetnie, że goście mogą zapisywać wiadomości w bazie danych, ale nie widzą ich jeszcze w aplikacji. Czas to naprawić.
Synchronizacja wiadomości
Aby wyświetlać wiadomości, musisz dodać detektory, które uruchamiają się po zmianie danych, a następnie utworzyć element interfejsu pokazujący nowe wiadomości. Do stanu aplikacji dodajesz kod, który nasłuchuje nowo dodanych komunikatów z aplikacji.
- Utwórz nowy plik
guest_book_message.dart
i dodaj klasę poniżej, aby udostępnić uporządkowany widok danych, które przechowujesz w Firestore.
lib/guest_book_message.dart
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- W pliku
lib/app_state.dart
dodaj te operacje importu:
lib/app_state.dart,
import 'dart:async'; // new
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
import 'guest_book_message.dart'; // new
- W sekcji
ApplicationState
, w której definiujesz stan i metody dostępu, dodaj te wiersze:
lib/app_state.dart
bool _loggedIn = false;
bool get loggedIn => _loggedIn;
// Add from here...
StreamSubscription<QuerySnapshot>? _guestBookSubscription;
List<GuestBookMessage> _guestBookMessages = [];
List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
// ...to here.
- W sekcji inicjowania
ApplicationState
dodaj te wiersze, aby zasubskrybować zapytanie obejmujące kolekcję dokumentów w przypadku zalogowania się użytkownika i anulowania subskrypcji po wylogowaniu:
lib/app_state.dart
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
} else {
_loggedIn = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
}
notifyListeners();
});
}
Ta sekcja jest ważna, ponieważ to w niej tworzysz zapytanie dotyczące kolekcji guestbook
oraz obsługujesz subskrypcje i anulowania subskrypcji tej kolekcji. Odsłuchujesz strumień, w którym odtwarzasz lokalny bufor wiadomości z kolekcji guestbook
, a także przechowujesz odwołanie do tej subskrypcji, aby móc ją później anulować. Wiele się tutaj dzieje, więc użyj debugera i sprawdź, co się dzieje, aby uzyskać lepszy model psychiczny. Więcej informacji znajdziesz w artykule Otrzymywanie aktualizacji w czasie rzeczywistym za pomocą Firestore.
- W pliku
lib/guest_book.dart
dodaj ten import:
import 'guest_book_message.dart';
- W ramach konfiguracji widgetu
GuestBook
dodaj listę wiadomości, aby połączyć ten zmieniający się stan z interfejsem użytkownika:
lib/książka_guest.dart
class GuestBook extends StatefulWidget {
// Modify the following line:
const GuestBook({
super.key,
required this.addMessage,
required this.messages,
});
final FutureOr<void> Function(String message) addMessage;
final List<GuestBookMessage> messages; // new
@override
_GuestBookState createState() => _GuestBookState();
}
- Aby udostępnić tę konfigurację, w klasie
_GuestBookState
zmodyfikuj metodębuild
w ten sposób:
lib/książka_guest.dart
class _GuestBookState extends State<GuestBook> {
final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
final _controller = TextEditingController();
@override
// Modify from here...
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ...to here.
Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Leave a message',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter your message to continue';
}
return null;
},
),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await widget.addMessage(_controller.text);
_controller.clear();
}
},
child: Row(
children: const [
Icon(Icons.send),
SizedBox(width: 4),
Text('SEND'),
],
),
),
],
),
),
),
// Modify from here...
const SizedBox(height: 8),
for (var message in widget.messages)
Paragraph('${message.name}: ${message.message}'),
const SizedBox(height: 8),
],
// ...to here.
);
}
}
Poprzednią zawartość metody build()
otaczasz widżetem Column
, a potem dodasz kolekcję na końcu elementów podrzędnych Column
, aby wygenerować nową wartość Paragraph
dla każdej wiadomości na liście wiadomości.
- Zaktualizuj treść parametru
HomePage
, aby prawidłowo utworzyćGuestBook
za pomocą nowego parametrumessages
:
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.loggedIn) ...[
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages, // new
),
],
],
),
),
Synchronizacja wiadomości testowych
Firestore automatycznie i natychmiast synchronizuje dane z klientami subskrybującymi bazę danych.
Testowanie synchronizacji wiadomości:
- W aplikacji znajdź wiadomości utworzone wcześniej w bazie danych.
- Napisz nowe wiadomości. Pojawiają się od razu.
- Otwórz obszar roboczy w wielu oknach lub kartach. Wiadomości są synchronizowane w czasie rzeczywistym między oknami i kartami.
- Opcjonalnie: w menu Baza danych konsoli Firebase ręcznie usuwaj, modyfikuj i dodaj nowe wiadomości. Wszystkie zmiany są widoczne w interfejsie.
Gratulacje! Dokumenty Firestore zostały odczytywane w aplikacji.
Podgląd aplikacji
8. Konfigurowanie podstawowych reguł zabezpieczeń
Wstępnie skonfigurowano w Firestore tryb testowy, co oznacza, że baza danych jest otwarta do odczytu i zapisu. Tryb testowy należy jednak stosować tylko na wczesnych etapach rozwoju. Najlepiej jest skonfigurować reguły bezpieczeństwa bazy danych podczas tworzenia aplikacji. Bezpieczeństwo jest integralną częścią struktury i działania aplikacji.
Reguły zabezpieczeń Firebase umożliwiają kontrolowanie dostępu do dokumentów i kolekcji w bazie danych. Elastyczna składnia reguł umożliwia tworzenie reguł, które pasują do wszystkich zapisów, od wszystkich zapisów do całej bazy danych po operacje na określonym dokumencie.
Skonfiguruj podstawowe reguły zabezpieczeń:
- W menu Programowanie konsoli Firebase kliknij Baza danych > Reguły. Powinny być widoczne te domyślne reguły zabezpieczeń oraz ostrzeżenie o tym, że są one publiczne:
- Określ kolekcje, w których aplikacja zapisuje dane:
W sekcji match /databases/{database}/documents
zidentyfikuj kolekcję, którą chcesz zabezpieczyć:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
// You'll add rules here in the next step.
}
}
W każdym dokumencie w księdze gości używasz identyfikatora UID uwierzytelniania, więc możesz go pobrać i sprawdzić, czy każdy, kto próbuje zapisać w nim dane, ma zgodny identyfikator UID uwierzytelniania.
- Dodaj reguły odczytu i zapisu do zestawu reguł:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
}
Obecnie tylko zalogowani użytkownicy mogą czytać wiadomości w księdze gości, ale tylko jej autor może edytować wiadomość.
- Dodaj walidację danych, aby mieć pewność, że w dokumencie znajdują się wszystkie oczekiwane pola:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId
&& "name" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
}
9. Dodatkowy krok: ćwicz zdobytą wiedzę w praktyce
Nagrywanie stanu odpowiedzi uczestnika
Obecnie Twoja aplikacja pozwala użytkownikom na prowadzenie czatu tylko wtedy, gdy są zainteresowani wydarzeniem. O tym, czy ktoś bierze udział w spotkaniu, można się dowiedzieć tylko wtedy, gdy jego głos mówi na czacie.
W tym kroku musisz się zorganizować i poinformować uczestników, ilu ich będzie. Do stanu aplikacji dodajesz kilka funkcji. Pierwsza to możliwość określenia przez zalogowanego użytkownika, czy zamierza wziąć udział w wydarzeniu. Drugi to licznik, który pokazuje, ile osób weźmie udział.
- W pliku
lib/app_state.dart
dodaj te wiersze do sekcjiApplicationState
w plikuApplicationState
, aby kod interfejsu użytkownika mógł wchodzić w interakcje z tym stanem:
lib/app_state.dart,
int _attendees = 0;
int get attendees => _attendees;
Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
final userDoc = FirebaseFirestore.instance
.collection('attendees')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (attending == Attending.yes) {
userDoc.set(<String, dynamic>{'attending': true});
} else {
userDoc.set(<String, dynamic>{'attending': false});
}
}
- Zaktualizuj metodę
init()
metodyApplicationState
w ten sposób:
lib/app_state.dart
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
// Add from here...
FirebaseFirestore.instance
.collection('attendees')
.where('attending', isEqualTo: true)
.snapshots()
.listen((snapshot) {
_attendees = snapshot.docs.length;
notifyListeners();
});
// ...to here.
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
_emailVerified = user.emailVerified;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
// Add from here...
_attendingSubscription = FirebaseFirestore.instance
.collection('attendees')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['attending'] as bool) {
_attending = Attending.yes;
} else {
_attending = Attending.no;
}
} else {
_attending = Attending.unknown;
}
notifyListeners();
});
// ...to here.
} else {
_loggedIn = false;
_emailVerified = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
_attendingSubscription?.cancel(); // new
}
notifyListeners();
});
}
Ten kod dodaje zawsze subskrybowane zapytanie w celu określenia liczby uczestników oraz drugie zapytanie, które jest aktywne tylko wtedy, gdy użytkownik jest zalogowany. W ten sposób określa, czy użytkownik bierze udział.
- Na początku pliku
lib/app_state.dart
dodaj podane niżej wyliczenie.
lib/app_state.dart,
enum Attending { yes, no, unknown }
- Utwórz nowy plik
yes_no_selection.dart
, zdefiniuj nowy widżet, który będzie działać jak przyciski opcji:
lib/yes_no_selection.dart,
import 'package:flutter/material.dart';
import 'app_state.dart';
import 'src/widgets.dart';
class YesNoSelection extends StatelessWidget {
const YesNoSelection(
{super.key, required this.state, required this.onSelection});
final Attending state;
final void Function(Attending selection) onSelection;
@override
Widget build(BuildContext context) {
switch (state) {
case Attending.yes:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
FilledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
case Attending.no:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
default:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
}
}
}
Na początku jest nieokreślony i nie wybrano Tak ani Nie. Gdy użytkownik wybierze, czy chce wziąć udział, ta opcja zostanie podświetlona jako wypełniony przycisk, a druga opcja będzie renderowana płynnie.
- Zaktualizuj metodę
HomePage
build()
, aby korzystać z funkcjiYesNoSelection
, umożliwić zalogowanemu użytkownikowi określenie, czy zamierza wziąć udział w wydarzeniu, oraz wyświetlać liczbę uczestników wydarzenia:
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add from here...
switch (appState.attendees) {
1 => const Paragraph('1 person going'),
>= 2 => Paragraph('${appState.attendees} people going'),
_ => const Paragraph('No one going'),
},
// ...to here.
if (appState.loggedIn) ...[
// Add from here...
YesNoSelection(
state: appState.attending,
onSelection: (attending) => appState.attending = attending,
),
// ...to here.
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages,
),
],
],
),
),
Dodaj reguły
Niektóre reguły zostały już skonfigurowane, więc dane dodane za pomocą tych przycisków zostaną odrzucone. Musisz zaktualizować reguły, aby umożliwić dodawanie plików do kolekcji attendees
.
- W kolekcji
attendees
znajdź identyfikator UID użyty jako nazwa dokumentu i sprawdź, czy identyfikatoruid
osoby przesyłającej jest taki sam jak dokument, który pisze:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
Dzięki temu wszyscy będą mogli czytać listę uczestników, ponieważ nie zawierają one prywatnych danych, a jedynie twórca może je aktualizować.
- Dodaj funkcję sprawdzania poprawności danych, aby mieć pewność, że w dokumencie znajdują się wszystkie oczekiwane pola:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId
&& "attending" in request.resource.data;
}
}
}
- Opcjonalnie: w aplikacji klikaj przyciski, aby wyświetlić wyniki w panelu Firestore w konsoli Firebase.
Podgląd aplikacji
10. Gratulacje!
Udało Ci się wykorzystać Firebase do utworzenia interaktywnej aplikacji internetowej działającej w czasie rzeczywistym.