Poznaj Firebase dla Flutter

1. Zanim zaczniesz

W tym laboratorium kodowania nauczysz się podstaw Firebase , aby tworzyć aplikacje mobilne Flutter na Androida i iOS.

Wymagania wstępne

Czego się nauczysz

  • Jak stworzyć aplikację do potwierdzania zgłoszeń i czatu w księdze gości na Androida, iOS, Internet i macOS za pomocą Flutter.
  • Jak uwierzytelniać użytkowników za pomocą uwierzytelniania Firebase i synchronizować dane z Firestore.

Ekran główny aplikacji na Androida

Ekran główny aplikacji na iOS

Co będziesz potrzebował

Dowolne z następujących urządzeń:

  • Fizyczne urządzenie z systemem Android lub iOS podłączone do komputera i ustawione w trybie programisty.
  • Symulator systemu iOS (wymaga narzędzi Xcode ).
  • Emulator Androida (wymaga konfiguracji w Android Studio ).

Potrzebujesz również:

  • Wybrana przeglądarka, na przykład Google Chrome.
  • Wybrane środowisko IDE lub edytor tekstu skonfigurowany z wtyczkami Dart i Flutter, na przykład Android Studio lub Visual Studio Code .
  • Najnowsza stable wersja Fluttera lub beta , jeśli lubisz życie na krawędzi.
  • Konto Google do tworzenia projektu Firebase i zarządzania nim.
  • Firebase CLI zalogował się na Twoje konto Google.

2. Pobierz przykładowy kod

Pobierz wstępną wersję swojego projektu z GitHub:

  1. Z wiersza poleceń sklonuj repozytorium GitHub w katalogu flutter-codelabs :
git clone https://github.com/flutter/codelabs.git flutter-codelabs

Katalog flutter-codelabs zawiera kod kolekcji codelabs. Kod tego laboratorium kodów znajduje się w katalogu flutter-codelabs/firebase-get-to-know-flutter . Katalog zawiera serię migawek, które pokazują, jak projekt powinien wyglądać na końcu każdego kroku. Na przykład jesteś na drugim etapie.

  1. Znajdź pliki pasujące do drugiego kroku:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Jeśli chcesz przeskoczyć do przodu lub zobaczyć jak coś powinno wyglądać po kroku, zajrzyj do katalogu o nazwie odpowiadającej krokowi, który Cię interesuje.

Zaimportuj aplikację startową

  • Otwórz lub zaimportuj flutter-codelabs/firebase-get-to-know-flutter/step_02 w preferowanym środowisku IDE. Ten katalog zawiera kod startowy dla codelab, który składa się z niedziałającej jeszcze aplikacji Flutter meetup.

Znajdź pliki, które wymagają pracy

Kod w tej aplikacji jest rozłożony na wiele katalogów. Ten podział funkcjonalności ułatwia pracę, ponieważ grupuje kod według funkcjonalności.

  • Zlokalizuj 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. Tworzą ekran aplikacji startowej.
    • lib/src/authentication.dart : Ten plik zawiera częściową implementację uwierzytelniania z zestawem widżetów służących do tworzenia środowiska logowania użytkownika do uwierzytelniania opartego na e-mailach Firebase. Te widżety przepływu uwierzytelniania nie są jeszcze używane w aplikacji startowej, ale wkrótce je dodasz.

W razie potrzeby dodajesz dodatkowe pliki, aby skompilować resztę aplikacji.

Przejrzyj plik lib/main.dart

Ta aplikacja korzysta z pakietu google_fonts , aby Roboto była domyślną czcionką w całej aplikacji. Możesz przeglądać fonts.google.com i używać znalezionych tam czcionek w różnych częściach aplikacji.

Używasz widżetów pomocniczych z pliku lib/src/widgets.dart w postaci Header , Paragraph i IconAndDetail . Te widżety eliminują zduplikowany kod, aby zmniejszyć bałagan w układzie strony opisanym w HomePage . Umożliwia to również spójny wygląd i styl.

Oto jak wygląda Twoja aplikacja na Androidzie, iOS, w Internecie i macOS:

Ekran główny aplikacji na Androida

Ekran główny aplikacji na iOS

Ekran główny aplikacji w internecie

Ekran główny aplikacji w systemie macOS

3. Utwórz i skonfiguruj projekt Firebase

Wyświetlanie informacji o wydarzeniu jest świetne dla Twoich gości, ale samo w sobie nie jest zbyt przydatne dla nikogo. Musisz dodać dynamiczną funkcjonalność do aplikacji. Aby to zrobić, musisz połączyć Firebase ze swoją aplikacją. Aby rozpocząć korzystanie z Firebase, musisz utworzyć i skonfigurować projekt Firebase.

Utwórz projekt Firebase

  1. Zaloguj się do Firebase .
  2. W konsoli kliknij Dodaj projekt lub Utwórz projekt .
  3. W polu Nazwa projektu wpisz Firebase-Flutter-Codelab , a następnie kliknij Kontynuuj .

4395e4e67c08043a.png

  1. Kliknij opcje tworzenia projektu. Jeśli pojawi się monit, zaakceptuj warunki Firebase, ale pomiń konfigurację Google Analytics, ponieważ nie będziesz go używać w tej aplikacji.

b7138cde5f2c7b61.png

Aby dowiedzieć się więcej o projektach Firebase, zobacz Omówienie projektów Firebase .

Aplikacja korzysta z następujących produktów Firebase, które są dostępne dla aplikacji internetowych:

  • Uwierzytelnianie: umożliwia użytkownikom logowanie się do Twojej aplikacji.
  • Firestore: zapisuje uporządkowane dane w chmurze i otrzymuje natychmiastowe powiadomienia o zmianie danych.
  • Reguły bezpieczeństwa Firebase: Zabezpiecza Twoją bazę danych.

Niektóre z tych produktów wymagają specjalnej konfiguracji lub musisz je włączyć w konsoli Firebase.

Włącz uwierzytelnianie logowania przez e-mail

  1. W okienku Przegląd projektu w konsoli Firebase rozwiń menu Kompilacja .
  2. Kliknij opcję Uwierzytelnianie > Rozpocznij > Metoda logowania > Adres e-mail/hasło > Włącz > Zapisz .

58e3e3e23c2f16a4.png

Włącz Firestore

Aplikacja internetowa używa Firestore do zapisywania wiadomości na czacie i odbierania nowych wiadomości na czacie.

Włącz Firestore:

  • W menu Build kliknij Cloud Firestore > Utwórz bazę danych .

99e8429832d23fa3.png

  1. Wybierz Uruchom w trybie testowym , a następnie przeczytaj zastrzeżenie dotyczące zasad bezpieczeństwa. Tryb testowy zapewnia, że ​​możesz swobodnie pisać do bazy danych podczas programowania.

6be00e26c72ea032.png

  1. Kliknij Dalej , a następnie wybierz lokalizację dla swojej bazy danych. Możesz użyć domyślnego. Nie możesz później zmienić lokalizacji.

278656eefcfb0216.png

  1. Kliknij Włącz .

4. Skonfiguruj Firebase

Aby używać Firebase z Flutter, musisz wykonać następujące zadania, aby skonfigurować projekt Flutter do prawidłowego korzystania z bibliotek FlutterFire :

  1. Dodaj zależności FlutterFire do swojego projektu.
  2. Zarejestruj wybraną platformę w projekcie Firebase.
  3. Pobierz plik konfiguracyjny specyficzny dla platformy, a następnie dodaj go do kodu.

W katalogu najwyższego poziomu Twojej aplikacji Flutter znajdują się podkatalogi android , ios , macos i web , które przechowują pliki konfiguracyjne specyficzne dla platformy odpowiednio dla iOS i Androida.

Skonfiguruj zależności

Musisz dodać biblioteki FlutterFire dla dwóch produktów Firebase, których używasz w tej aplikacji: Authentication i Firestore.

  • Z wiersza poleceń dodaj następujące zależności:
$ flutter pub add firebase_core

Pakiet firebase_core to wspólny kod wymagany dla wszystkich wtyczek Firebase Flutter.

$ flutter pub add firebase_auth

Pakiet firebase_auth umożliwia integrację z uwierzytelnianiem.

$ flutter pub add cloud_firestore

Pakiet cloud_firestore umożliwia dostęp do magazynu danych Firestore.

$ flutter pub add provider

Pakiet firebase_ui_auth zawiera zestaw widżetów i narzędzi zwiększających prędkość programisty dzięki przepływom uwierzytelniania.

$ flutter pub add firebase_ui_auth

Dodałeś wymagane pakiety, ale musisz także skonfigurować projekty iOS, Android, macOS i Web runner, aby odpowiednio używać Firebase. Korzystasz również z pakietu provider , który umożliwia oddzielenie logiki biznesowej od logiki wyświetlania.

Zainstaluj interfejs wiersza polecenia FlutterFire

Interfejs wiersza polecenia FlutterFire zależy od bazowego interfejsu wiersza polecenia Firebase.

  1. Jeśli jeszcze tego nie zrobiłeś, zainstaluj Firebase CLI na swoim komputerze.
  2. Zainstaluj interfejs wiersza polecenia FlutterFire:
$ dart pub global activate flutterfire_cli

Po zainstalowaniu polecenie flutterfire jest dostępne na całym świecie.

Skonfiguruj swoje aplikacje

CLI wyodrębnia informacje z Twojego projektu Firebase i wybranych aplikacji projektu, aby wygenerować całą konfigurację dla określonej platformy.

W katalogu głównym swojej aplikacji uruchom komendę configure :

$ flutterfire configure

Polecenie konfiguracji prowadzi użytkownika przez następujące procesy:

  1. Wybierz projekt Firebase na podstawie pliku .firebaserc lub z konsoli Firebase.
  2. Określ platformy do konfiguracji, takie jak Android, iOS, macOS i Web.
  3. Zidentyfikuj aplikacje Firebase, z których chcesz wyodrębnić konfigurację. Domyślnie interfejs CLI próbuje automatycznie dopasować aplikacje Firebase na podstawie bieżącej konfiguracji projektu.
  4. Wygeneruj plik firebase_options.dart w swoim projekcie.

Skonfiguruj macOS

Flutter na macOS buduje aplikacje w pełni piaskownicy. Ponieważ ta aplikacja integruje się z siecią, aby komunikować się z serwerami Firebase, musisz skonfigurować swoją aplikację z uprawnieniami klienta sieciowego.

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>

Aby uzyskać więcej informacji, zobacz Obsługa aplikacji Flutter dla komputerów stacjonarnych .

5. Dodaj funkcję RSVP

Teraz, po dodaniu Firebase do aplikacji, możesz utworzyć przycisk RSVP , który rejestruje osoby z uwierzytelnianiem . W przypadku natywnego systemu Android, natywnego systemu iOS i sieci Web dostępne są gotowe pakiety FirebaseUI Auth , ale musisz zbudować tę możliwość dla Flutter.

Pobrany wcześniej projekt zawierał zestaw widżetów implementujących interfejs użytkownika dla większości przepływu uwierzytelniania. Implementujesz logikę biznesową, aby zintegrować uwierzytelnianie z aplikacją.

Dodaj logikę biznesową za pomocą pakietu Provider

Użyj pakietu provider , aby udostępnić scentralizowany obiekt stanu aplikacji w drzewie widżetów Flutter:

  1. Utwórz nowy plik o nazwie app_state.dart o następującej treści:

lib/stan_aplikacji.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, pobierają pakiet provider , który udostępnia obiekt stanu aplikacji w całym drzewie widżetów i zawierają widżety uwierzytelniania z pakietu firebase_ui_auth .

Ten obiekt stanu aplikacji ApplicationState ma jedną główną odpowiedzialność za ten krok, czyli powiadomienie drzewa widżetów, że nastąpiła aktualizacja stanu uwierzytelnionego.

Używasz tylko dostawcy do przekazywania stanu statusu logowania użytkownika do aplikacji. Aby umożliwić użytkownikowi zalogowanie się, korzystasz z interfejsów użytkownika dostarczanych przez pakiet firebase_ui_auth , który jest świetnym sposobem na szybkie ładowanie ekranów logowania w aplikacjach.

Zintegruj przepływ uwierzytelniania

  1. Zmodyfikuj 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';
  1. Połącz stan aplikacji z inicjalizacją aplikacji, a następnie dodaj przepływ 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() powoduje, że pakiet dostawcy jest odpowiedzialny za utworzenie instancji obiektu stanu aplikacji za pomocą widżetu ChangeNotifierProvider . Używasz tej konkretnej klasy provider , ponieważ obiekt stanu aplikacji rozszerza klasę ChangeNotifier , która informuje pakiet provider , kiedy ponownie wyświetlić zależne widżety.

  1. Zaktualizuj swoją aplikację, aby obsługiwała nawigację do różnych ekranów udostępnianych przez FirebaseUI, tworząc 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.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
    );
  }
}

Każdy ekran ma skojarzony z nim inny typ akcji na podstawie nowego stanu przepływu uwierzytelniania. Po większości zmian stanu uwierzytelniania możesz przekierować z powrotem do preferowanego ekranu, niezależnie od tego, czy jest to ekran główny, czy inny ekran, na przykład profil.

  1. W metodzie budowania klasy HomePage zintegruj stan aplikacji z widżetem AuthFunc :

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 opakowujesz go w widżet Consumer . Widżet Konsument to zwykły sposób użycia pakietu provider do odbudowania części drzewa po zmianie stanu aplikacji. Widżet AuthFunc to dodatkowe widżety, które testujesz.

Przetestuj przepływ uwierzytelniania

cdf2d25e436bd48d.png

  1. W aplikacji dotknij przycisku RSVP , aby zainicjować SignInScreen .

2a2cd6d69d172369.png

  1. Wprowadź adres email. Jeśli jesteś już zarejestrowany, system poprosi o podanie hasła. W innym przypadku system poprosi o wypełnienie formularza rejestracyjnego.

e5e65065dba36b54.png

  1. Wprowadź hasło krótsze niż sześć znaków, aby sprawdzić przepływ obsługi błędów. Jeśli jesteś zarejestrowany, zamiast tego zobaczysz hasło.
  2. Wprowadź nieprawidłowe hasła, aby sprawdzić przepływ obsługi błędów.
  3. Wprowadź poprawne hasło. Zobaczysz środowisko zalogowania, które oferuje użytkownikowi możliwość wylogowania.

4ed811a25b0cf816.png

6. Pisz wiadomości do Firestore

Wspaniale jest wiedzieć, że użytkownicy przychodzą, ale musisz dać gościom coś innego do zrobienia w aplikacji. Co by było, gdyby mogli zostawiać wiadomości w księdze gości? Mogą powiedzieć, dlaczego nie mogą się doczekać przyjazdu lub kogo mają nadzieję spotkać.

Do przechowywania wiadomości czatu, które użytkownicy piszą w aplikacji, używasz Firestore .

Model danych

Firestore to baza danych NoSQL, a dane przechowywane w bazie danych są podzielone na kolekcje, dokumenty, pola i podkolekcje. Każdą wiadomość czatu przechowujesz jako dokument w kolekcji guestbook , która jest kolekcją najwyższego poziomu.

7c20dc8424bb1d84.png

Dodaj wiadomości do Firestore

W tej sekcji dodasz funkcję umożliwiającą użytkownikom zapisywanie komunikatów w bazie danych. Najpierw dodajesz pole formularza i przycisk wyślij, a następnie kod łączący te elementy z bazą danych.

  1. Utwórz nowy plik o nazwie guest_book.dart , dodaj stanowy widżet GuestBook , aby utworzyć elementy interfejsu użytkownika pola wiadomości i przycisku wysyłania:

lib/księga_gości.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'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Jest tu kilka ciekawostek. Najpierw tworzysz instancję formularza, aby sprawdzić, czy wiadomość rzeczywiście zawiera treść, i pokazać użytkownikowi komunikat o błędzie, jeśli go nie ma. Aby zweryfikować formularz, uzyskujesz dostęp do stanu formularza za formularzem za pomocą GlobalKey . Aby uzyskać więcej informacji o kluczach i sposobach ich używania, zobacz Kiedy używać kluczy .

Zwróć również uwagę na sposób rozmieszczenia widżetów, masz Row z TextFormField i StyledButton , który zawiera Row . Należy również zauważyć, że TextFormField jest opakowany w widżet Expanded , co zmusza TextFormField do wypełnienia dodatkowego miejsca w wierszu. Aby lepiej zrozumieć, dlaczego jest to wymagane, zobacz Zrozumienie ograniczeń .

Teraz, gdy masz widżet, który umożliwia użytkownikowi wprowadzenie tekstu do Księgi Gości, musisz go wyświetlić na ekranie.

  1. Edytuj treść HomePage , aby dodać następujące dwa wiersze na końcu elementów podrzędnych ListView :
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)),

To wystarczy do wyświetlenia widżetu, ale nie wystarczy do zrobienia czegokolwiek pożytecznego. Wkrótce zaktualizujesz ten kod, aby działał.

Podgląd aplikacji

Ekran główny aplikacji na Androida z integracją czatu

Ekran główny aplikacji na iOS z integracją czatu

Ekran główny aplikacji w Internecie z integracją czatu

Ekran główny aplikacji na macOS z integracją czatu

Gdy użytkownik kliknie WYŚLIJ , uruchamia następujący fragment kodu. Dodaje zawartość pola wprowadzania wiadomości do kolekcji guestbook w bazie 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 jest odniesieniem do automatycznie generowanego unikalnego identyfikatora, który uwierzytelnianie nadaje wszystkim zalogowanym użytkownikom.

  • W pliku lib/app_state.dart dodaj metodę addMessageToGuestBook . W następnym kroku połączysz tę możliwość z interfejsem użytkownika.

lib/stan_aplikacji.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.
}

Połącz interfejs użytkownika i bazę danych

Masz interfejs użytkownika, w którym użytkownik może wprowadzić tekst, który chce dodać do Księgi Gości, i masz kod, aby dodać wpis do Firestore. Teraz wszystko, co musisz zrobić, to połączyć oba.

  • W pliku lib/home_page.dart wprowadź następującą zmianę w widgecie HomePage :

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.
        ],
      ),
    );
  }
}

Zastąpiłeś dwie linie dodane na początku tego kroku pełną implementacją. Ponownie używasz Consumer<ApplicationState> , aby udostępnić stan aplikacji dla renderowanej części drzewa. Dzięki temu możesz zareagować na kogoś, kto wpisze wiadomość w interfejsie użytkownika i opublikuje ją w bazie danych. W następnej sekcji sprawdzisz, czy dodane wiadomości są publikowane w bazie danych.

Testuj wysyłanie wiadomości

  1. W razie potrzeby zaloguj się do aplikacji.
  2. Wprowadź wiadomość, na przykład Hey there! , a następnie kliknij WYŚLIJ .

Ta czynność powoduje zapisanie wiadomości w bazie danych Firestore. Jednak nie widzisz komunikatu w rzeczywistej aplikacji Flutter, ponieważ nadal musisz zaimplementować pobieranie danych, co robisz w następnym kroku. Jednak w panelu bazy 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 swojej kolekcji guestbook . Na przykład zobacz następujący fragment kodu:

713870af0b3b63c.png

7. Czytaj wiadomości

To cudowne, że goście mogą pisać wiadomości do bazy danych, ale nie widzą ich jeszcze w aplikacji. Czas to naprawić!

Synchronizuj wiadomości

Aby wyświetlać komunikaty, musisz dodać odbiorniki uruchamiające się w przypadku zmiany danych, a następnie utworzyć element interfejsu użytkownika, który wyświetla nowe komunikaty. Do stanu aplikacji dodajesz kod, który nasłuchuje nowo dodanych komunikatów z aplikacji.

  1. Utwórz nowy plik guest_book_message.dart , dodaj poniższą klasę, aby udostępnić uporządkowany widok danych przechowywanych w Firestore.

lib/księga_gości_wiadomości.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});

  final String name;
  final String message;
}
  1. W pliku lib/app_state.dart dodaj następujące importy:

lib/stan_aplikacji.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
  1. W sekcji ApplicationState , w której definiujesz stan i gettery, dodaj następujące linie:

lib/stan_aplikacji.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. W sekcji inicjalizacji ApplicationState dodaj następujące wiersze, aby subskrybować zapytanie dotyczące kolekcji dokumentów, gdy użytkownik się loguje i anulować subskrypcję, gdy się wyloguje:

lib/stan_aplikacji.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ż tworzysz zapytanie dotyczące kolekcji guestbook i obsługujesz subskrypcję i anulowanie subskrypcji tej kolekcji. Słuchasz strumienia, w którym rekonstruujesz lokalną pamięć podręczną wiadomości w kolekcji guestbook , a także przechowujesz odniesienie do tej subskrypcji, aby móc później z niej zrezygnować. Dużo się tu dzieje, więc powinieneś zbadać to w debugerze, aby sprawdzić, co się dzieje, aby uzyskać wyraźniejszy model mentalny. Aby uzyskać więcej informacji, zobacz Pobieranie aktualizacji w czasie rzeczywistym za pomocą Firestore .

  1. W pliku lib/guest_book.dart dodaj następujący import:
import 'guest_book_message.dart';
  1. W widżecie GuestBook dodaj listę wiadomości jako część konfiguracji, aby połączyć ten zmieniający się stan z interfejsem użytkownika:

lib/księga_gości.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();
}
  1. W _GuestBookState zmodyfikuj metodę build w następujący sposób, aby udostępnić tę konfigurację:

lib/księga_gości.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.
    );
  }
}

Zawijasz poprzednią zawartość metody build() widżetem Column , a następnie dodajesz kolekcję dla na końcu elementów podrzędnych Column , aby wygenerować nowy Paragraph dla każdej wiadomości na liście wiadomości.

  1. Zaktualizuj treść HomePage , aby poprawnie skonstruować GuestBook za pomocą nowego parametru messages :

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
        ),
      ],
    ],
  ),
),

Przetestuj synchronizację wiadomości

Firestore automatycznie i błyskawicznie synchronizuje dane z klientami zapisanymi do bazy danych.

Synchronizacja wiadomości testowych:

  1. W aplikacji znajdź wiadomości utworzone wcześniej w bazie danych.
  2. Napisz nowe wiadomości. Pojawiają się natychmiast.
  3. Otwórz swój obszar roboczy w wielu oknach lub kartach. Wiadomości synchronizują się w czasie rzeczywistym w oknach i na kartach.
  4. Opcjonalnie: w menu bazy danych konsoli Firebase ręcznie usuń, zmodyfikuj lub dodaj nowe wiadomości. Wszystkie zmiany pojawiają się w interfejsie użytkownika.

Gratulacje! Czytasz dokumenty Firestore w swojej aplikacji!

Podgląd aplikacji

Ekran główny aplikacji na Androida z integracją czatu

Ekran główny aplikacji na iOS z integracją czatu

Ekran główny aplikacji w Internecie z integracją czatu

Ekran główny aplikacji na macOS z integracją czatu

8. Skonfiguruj podstawowe zasady bezpieczeństwa

Początkowo skonfigurowałeś Firestore do używania trybu testowego, co oznacza, że ​​Twoja baza danych jest otwarta do odczytu i zapisu. Należy jednak używać trybu testowego tylko na wczesnych etapach programowania. Najlepszym rozwiązaniem jest skonfigurowanie reguł bezpieczeństwa dla bazy danych podczas opracowywania aplikacji. Bezpieczeństwo jest integralną częścią struktury i zachowania Twojej aplikacji.

Reguły bezpieczeństwa Firebase pozwalają kontrolować dostęp do dokumentów i kolekcji w bazie danych. Elastyczna składnia reguł pozwala tworzyć reguły pasujące do wszystkiego, od wszystkich zapisów w całej bazie danych po operacje na określonym dokumencie.

Skonfiguruj podstawowe zasady bezpieczeństwa:

  1. W menu Programowanie konsoli Firebase kliknij Baza danych > Reguły . Powinieneś zobaczyć następujące domyślne reguły bezpieczeństwa i ostrzeżenie o tym, że reguły są publiczne:

7767a2d2e64e7275.png

  1. Zidentyfikuj kolekcje, do których aplikacja zapisuje dane:

W match /databases/{database}/documents określ 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.
  }
}

Ponieważ identyfikator UID uwierzytelnienia został użyty jako pole w każdym dokumencie księgi gości, można uzyskać identyfikator UID uwierzytelnienia i sprawdzić, czy każdy, kto próbuje pisać w dokumencie, ma pasujący identyfikator UID uwierzytelnienia.

  1. Dodaj reguły odczytu i zapisu do swojego 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;
    }
  }
}

Teraz tylko zalogowani użytkownicy mogą czytać wiadomości w księdze gości, ale tylko autor wiadomości może edytować wiadomość.

  1. Dodaj walidację danych, aby upewnić się, ż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: przećwicz to, czego się nauczyłeś

Zapisz status RSVP uczestnika

W tej chwili Twoja aplikacja umożliwia czatowanie tylko osobom zainteresowanym wydarzeniem. Ponadto jedynym sposobem, aby dowiedzieć się, czy ktoś nadchodzi, jest powiadomienie o tym na czacie.

Na tym etapie organizujesz się i informujesz ludzi, ile osób przyjdzie. Do stanu aplikacji dodajesz kilka możliwości. Pierwszym z nich jest możliwość nominowania przez zalogowanego użytkownika, czy bierze udział. Drugi to licznik, ile osób uczestniczy.

  1. W pliku lib/app_state.dart dodaj następujące wiersze do sekcji akcesorów w ApplicationState , aby kod interfejsu użytkownika mógł wchodzić w interakcję z tym stanem:

lib/stan_aplikacji.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});
  }
}
  1. Zaktualizuj metodę init() klasy ApplicationState w następujący sposób:

lib/stan_aplikacji.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) {
        _loginState = ApplicationLoginState.loggedIn;
        _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 {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Ten kod dodaje zawsze subskrybowane zapytanie, aby określić liczbę uczestników, oraz drugie zapytanie, które jest aktywne tylko wtedy, gdy użytkownik jest zalogowany, aby określić, czy użytkownik uczestniczy.

  1. Dodaj następujące wyliczenie na początku pliku lib/app_state.dart .

lib/stan_aplikacji.dart

enum Attending { yes, no, unknown }
  1. Utwórz nowy plik yes_no_selection.dart , zdefiniuj nowy widżet, który 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'),
              ),
            ],
          ),
        );
    }
  }
}

Rozpoczyna się w stanie nieokreślonym bez wybrania opcji Tak lub Nie . Gdy użytkownik wybierze, czy bierze udział, pokazujesz tę opcję podświetloną wypełnionym przyciskiem, a drugą opcję cofa się z płaskim renderowaniem.

  1. Zaktualizuj metodę build() HomePage , aby skorzystać z YesNoSelection , umożliwić zalogowanemu użytkownikowi nominowanie, czy bierze udział, i wyświetlić 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

Skonfigurowałeś już pewne reguły, więc dane, które dodasz za pomocą przycisków, zostaną odrzucone. Musisz zaktualizować reguły, aby zezwolić na dodawanie elementów do kolekcji attendees .

  1. W kolekcji attendees pobierz identyfikator UID uwierzytelnienia użyty jako nazwa dokumentu i sprawdź, czy uid przesyłającego 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 każdy może przeczytać listę uczestników, ponieważ nie ma tam żadnych prywatnych danych, ale tylko twórca może ją aktualizować.

  1. Dodaj walidację danych, aby upewnić się, ż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;

    }
  }
}
  1. Opcjonalnie: w aplikacji kliknij przyciski, aby zobaczyć wyniki w panelu Firestore w konsoli Firebase.

Podgląd aplikacji

Ekran główny aplikacji na Androida

Ekran główny aplikacji na iOS

Ekran główny aplikacji w internecie

Ekran główny aplikacji w systemie macOS

10. Gratulacje!

Użyłeś Firebase do zbudowania interaktywnej aplikacji internetowej działającej w czasie rzeczywistym!

Ucz się więcej