Poznaj Firebase dla Flutter

1. Zanim zaczniesz

W tym laboratorium programistycznym poznasz podstawy Firebase, aby tworzyć aplikacje mobilne Flutter na Androida i iOS.

Wymagania wstępne

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.

Ekran główny aplikacji na Androidzie

Ekran główny aplikacji na iOS

Czego potrzebujesz

Dowolne z tych urządzeń:

  • Fizyczne urządzenie z Androidem lub iOS połączone z komputerem i ustawione w trybie dewelopera.
  • Symulator iOS (wymaga narzędzi Xcode).
  • emulator Androida (wymaga konfiguracji w Android Studio);

Potrzebujesz też:

  • przeglądarkę, np. Google Chrome;
  • dowolny IDE lub edytor tekstu skonfigurowany z pluginami Dart i Flutter, np. Android Studio lub Visual Studio Code;
  • najnowszą wersję stable Fluttera lub beta, jeśli lubisz nowinki.
  • Konto Google do tworzenia projektu Firebase i zarządzania nim.
  • FirebaseCLI zalogowana na Twoje konto Google.

2. Pobieranie przykładowego kodu

Pobierz pierwszą wersję projektu z GitHuba:

  1. W wierszu poleceń skopiuj repozytorium GitHub do 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ę migawek pokazujących, jak projekt powinien wyglądać na końcu każdego kroku. Na przykład jesteś na 2. kroku.

  1. Znajdź pasujące pliki na potrzeby 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.

Importowanie aplikacji startowej

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

Znajdowanie plików, nad którymi trzeba popracować

Kod w tej aplikacji jest rozproszony po wielu katalogach. Taki podział funkcji ułatwia pracę, ponieważ kod jest grupowany według funkcji.

  • Znajdź te 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 tworzenie logowania dla uwierzytelniania e-mailowego Firebase. Te widżety procesu uwierzytelniania nie są jeszcze używane w aplikacji startowej, ale wkrótce je dodasz.

Dodaj dodatkowe pliki, które są wymagane do skompilowania 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łości aplikacji. Możesz przeglądać fonts.google.com i korzystać z czcionek, które tam znajdziesz, w różnych częściach aplikacji.

Używasz widżetów pomocniczych z pliku lib/src/widgets.dart w postaci Header, ParagraphIconAndDetail. Te widżety eliminują duplikowany kod, aby zmniejszyć ilość niepotrzebnych elementów w układzie strony opisanym w artykule HomePage. Dzięki temu możesz też uzyskać spójny wygląd i wrażenia.

Oto jak wygląda Twoja aplikacja na Androida, iOS, w internecie i na macOS:

Ekran główny aplikacji na Androidzie

Ekran główny aplikacji na iOS

Ekran główny aplikacji w wersji internetowej

Ekran główny aplikacji 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

  1. Zaloguj się w Firebase.
  2. W konsoli kliknij Dodaj projekt lub Utwórz projekt.
  3. W polu Nazwa projektu wpisz Firebase-Flutter-Codelab, a potem kliknij Dalej.

4395e4e67c08043a.png

  1. Przejrzyj opcje tworzenia projektu. 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.

b7138cde5f2c7b61.png

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: umożliwia użytkownikom logowanie się w aplikacji.
  • Firestore: przechowuje uporządkowane dane w chmurze i wysyła natychmiastowe powiadomienia o zmianach danych.
  • Reguły zabezpieczeń Firebase: zapewniają bezpieczeństwo Twojej bazie 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

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

58e3e3e23c2f16a4.png

Konfigurowanie Firestore

Aplikacja internetowa używa Firestore do zapisywania wiadomości czatu i otrzymywania nowych wiadomości czatu.

Aby skonfigurować Firestore w projekcie Firebase:

  1. W panelu po lewej stronie w konsoli Firebase rozwiń Kompilacja, a potem wybierz Baza danych Firestore.
  2. Kliknij Utwórz bazę danych.
  3. Pozostaw wartość (default) w polu Identyfikator bazy danych.
  4. Wybierz lokalizację bazy danych, a potem kliknij Dalej.
    W przypadku prawdziwej aplikacji wybierz lokalizację blisko użytkowników.
  5. 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.
  6. Kliknij Utwórz.

4. Konfigurowanie Firebase

Aby używać Firebase z Flutterem, musisz wykonać te czynności, aby skonfigurować projekt Fluttera tak, aby prawidłowo używał bibliotek FlutterFire:

  1. Dodaj do projektu zależności FlutterFire.
  2. Zarejestruj odpowiednią platformę w projekcie Firebase.
  3. Pobierz plik konfiguracji dla danej platformy, a potem dodaj go do kodu.

W katalogu najwyższego poziomu aplikacji Flutter znajdują się podkatalogi android, ios, macosweb, które zawierają pliki konfiguracji dla platformy iOS i Androida.

Konfigurowanie zależności

Musisz dodać biblioteki FlutterFire dla 2 usług Firebase, których używasz w tej aplikacji: Uwierzytelnianie i Firestore.

  • W wierszu poleceń dodaj te zależności:
$ flutter pub add firebase_core

Pakiet firebase_core to wspólny kod wymagany przez wszystkie wtyczki Firebase Flutter.

$ flutter pub add firebase_auth

Pakiet firebase_auth umożliwia integrację z usługą uwierzytelniania.

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

Pakiet firebase_ui_auth zawiera zestaw widżetów i narzędzi, które przyspieszają pracę deweloperó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.

  1. Jeśli jeszcze tego nie zrobiono, zainstaluj na komputerze interfejs wiersza poleceń Firebase.
  2. Zainstaluj interfejs wiersza poleceń FlutterFire:
$ dart pub global activate flutterfire_cli

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

Konfigurowanie aplikacji

Narzędzie wiersza poleceń wyodrębnia informacje z Twojego projektu Firebase i wybranych aplikacji w tym projekcie, aby wygenerować całą konfigurację dla konkretnej platformy.

W katalogu głównym aplikacji uruchom polecenie configure:

$ flutterfire configure

Polecenie konfiguracji przeprowadzi Cię przez te procesy:

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

Konfigurowanie systemu macOS

Flutter w systemie macOS tworzy aplikacje w pełni odizolowane. 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 Wsparcie dla Fluttera na komputerze.

5. Dodawanie funkcji odpowiedzi

Po dodaniu Firebase do aplikacji możesz utworzyć przycisk Potwierdź udział, który rejestruje osoby za pomocą usługi 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ę.

Projekt, który został wcześniej pobrany, zawierał zestaw widżetów implementujących interfejs użytkownika w większości procesu uwierzytelniania. Wdroż logikę biznesową, aby zintegrować uwierzytelnianie z aplikacją.

Dodawanie logiki biznesowej za pomocą pakietu Provider

Użyj pakietu provider, aby udostępnić centralny obiekt stanu aplikacji w całym drzewie widżetów Fluttera:

  1. 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, czyli ostrzeganie drzewa widżetów o zaktualizowanym stanie uwierzytelniania.

Używasz dostawcy tylko do przekazywania aplikacji stanu logowania użytkownika. Aby umożliwić użytkownikowi zalogowanie się, używasz interfejsu użytkownika udostępnianego przez pakiet firebase_ui_auth. Jest to świetny sposób na szybkie uruchamianie ekranów logowania w aplikacjach.

Integracja procesu uwierzytelniania

  1. 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';
  1. Połącz stan aplikacji z jej inicjalizacją, 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 jest odpowiedzialny za instancjowanie obiektu stanu aplikacji za pomocą widżetu ChangeNotifierProvider. Używasz tej konkretnej klasy provider, ponieważ obiekt stanu aplikacji rozszerza klasę ChangeNotifier, co pozwala pakietowi provider wiedzieć, kiedy ponownie wyświetlić zależne widżety.

  1. Zaktualizuj aplikację, aby obsługiwała nawigację do różnych ekranów udostępnianych przez 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 wrócić do preferowanego ekranu, np. ekranu głównego lub innego, np. profilu.

  1. W metodzie tworzenia 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 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

cdf2d25e436bd48d.png

  1. W aplikacji kliknij przycisk Odpowiedz, aby rozpocząć SignInScreen.

2a2cd6d69d172369.png

  1. Wpisz adres e-mail. Jeśli jesteś już zarejestrowany, system poprosi Cię o podanie hasła. W przeciwnym razie system poprosi Cię o wypełnienie formularza rejestracyjnego.

e5e65065dba36b54.png

  1. Aby sprawdzić przepływ pracy w przypadku 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.
  2. Aby sprawdzić przepływ pracy w przypadku błędów, wpisz nieprawidłowe hasła.
  3. Wpisz prawidłowe hasło. Widzisz widok po zalogowaniu się, który umożliwia użytkownikowi wylogowanie się.

4ed811a25b0cf816.png

6. Pisanie wiadomości do 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ą spotkać.

Aby przechowywać wiadomości czatu, które użytkownicy piszą w aplikacji, używasz Firestore.

Model danych

Firestore to baza danych NoSQL, a dane w niej przechowywane są podzielone na kolekcje, dokumenty, pola i podkolekcje. Każda wiadomość czatu jest przechowywana jako dokument w zbiorze guestbook, który jest zbiorem najwyższego poziomu.

7c20dc8424bb1d84.png

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.

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

lib/guest_book.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'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Tutaj jest kilka punktów zainteresowania. Najpierw utwórz instancję formularza, aby sprawdzić, czy wiadomość zawiera treść, i wyświetlić użytkownikowi komunikat o błędzie, jeśli jej nie ma. Aby sprawdzić poprawność formularza, uzyskaj dostęp do stanu formularza za pomocą elementu GlobalKey. Więcej informacji o kluczach i sposobie ich używania znajdziesz w artykule Kiedy używać kluczy.

Zwróć też uwagę na sposób rozmieszczenia widżetów: masz RowTextFormFieldStyledButton, który zawiera Row. Zwróć też uwagę, że element TextFormField jest ujęty w widżet Expanded, co powoduje, że element TextFormField wypełnia wszelkie dodatkowe miejsce 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.

  1. Zmień treść pliku HomePage, aby dodać te 2 wiersze na końcu elementów podrzędnych pliku 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, aby wyświetlić widżet, ale nie wystarczy do wykonania żadnej przydatnej czynności. Wkrótce zaktualizujesz ten kod, aby działał.

Podgląd aplikacji

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

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

Ekran główny aplikacji w przeglądarce z integracją czatu

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

Gdy użytkownik kliknie WYŚLIJ, zostanie uruchomiony ten fragment kodu. Dodaje zawartość pola wejściowego wiadomości do kolekcji guestbook bazy danych. Metoda addMessageToGuestBook dodaje treść wiadomości do nowego dokumentu z automatycznie wygenerowanym identyfikatorem w zbiorze 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. W następnym kroku połączysz tę funkcję z interfejsem użytkownika.

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 użytkownika, w którym może on wpisać tekst, który chce dodać do księgi gości, oraz kod, który służy do dodawania wpisów do Firestore. Teraz wystarczy połączyć te 2 elementy.

  • W pliku lib/home_page.dart wprowadź w widżecie HomePage 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żywasz Consumer<ApplicationState>, aby udostępnić stan aplikacji części drzewa, którą renderujesz. Dzięki temu możesz zareagować na osobę, która wpisze wiadomość w interfejsie, i opublikować ją w bazie danych. W następnej sekcji sprawdzisz, czy dodane wiadomości są publikowane w bazie danych.

Testowanie wysyłania wiadomości

  1. W razie potrzeby zaloguj się w aplikacji.
  2. Wpisz wiadomość, np. Hey there!, a następnie kliknij WYŚLIJ.

To działanie zapisuje wiadomość 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. Dodaną wiadomość znajdziesz jednak w kolekcjach guestbookpanelu bazy danych konsoli Firebase. Jeśli wyślesz więcej wiadomości, dodasz więcej dokumentów do kolekcji guestbook. Przykładowy fragment kodu:

713870af0b3b63c.png

7. Czytanie wiadomości

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

Synchronizacja wiadomości

Aby wyświetlać wiadomości, musisz dodać odbiorców, którzy będą reagować na zmiany danych, a potem utworzyć element interfejsu, który będzie wyświetlać nowe wiadomości. Do stanu aplikacji dodajesz kod, który nasłuchuje nowo dodanych wiadomości z aplikacji.

  1. Utwórz nowy plik guest_book_message.dart i dodaj do niego tę klasę, aby uzyskać uporządkowany widok danych przechowywanych w Firestore.

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. W pliku lib/app_state.dart dodaj te instrukcje 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
  1. 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.
  1. W sekcji inicjalizacji w ApplicationState dodaj te wiersze, aby subskrybować zapytanie dotyczące zbioru dokumentów, gdy użytkownik się zaloguje, i anulować subskrypcję, gdy się wyloguje:

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ć. Tutaj dzieje się dużo, więc warto zbadać to w debugerze, aby dokładniej zrozumieć, co się dzieje. Więcej informacji znajdziesz w artykule Otrzymywanie aktualizacji w czasie rzeczywistym za pomocą Firestore.

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

lib/guest_book.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. Aby udostępnić tę konfigurację, w klasie _GuestBookState zmodyfikuj metodę build w ten sposób:

lib/guest_book.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.

  1. Zaktualizuj treść tagu HomePage, aby prawidłowo utworzyć tag GuestBook z nowym parametrem 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
        ),
      ],
    ],
  ),
),

Synchronizacja wiadomości testowych

Firestore automatycznie i natychmiast synchronizuje dane z klientami subskrybowanymi w bazie danych.

Synchronizacja testowa wiadomości:

  1. W aplikacji odszukaj wiadomości utworzone wcześniej w bazie danych.
  2. pisać nowe wiadomości. Pojawiają się natychmiast.
  3. Otwórz obszar roboczy w wielu oknach lub kartach. Wiadomości są synchronizowane w czasie rzeczywistym w oknach i na kartach.
  4. Opcjonalnie: w menu Baza danych konsoli Firebase ręcznie usuwaj, modyfikuj i dodaj nowe wiadomości. Wszystkie zmiany są widoczne w interfejsie.

Gratulacje! odczytujesz dokumenty Firestore w aplikacji.

Podgląd aplikacji

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

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

Ekran główny aplikacji w przeglądarce z integracją czatu

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

8. Konfigurowanie podstawowych reguł zabezpieczeń

Początkowo Firestore jest skonfigurowany do korzystania z trybu testowego, co oznacza, że baza danych jest otwarta na odczyty i zapisy. 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 wszystkiego, od wszystkich zapisów w całej bazie danych po operacje na konkretnym dokumencie.

Skonfiguruj podstawowe reguły zabezpieczeń:

  1. W menu Rozwijaj konsoli Firebase kliknij Baza danych > Reguły. Powinny się tam wyświetlić te domyślne reguły zabezpieczeń i ostrzeżenie o ich dostępności dla wszystkich:

7767a2d2e64e7275.png

  1. Określ kolekcje, do 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.
  }
}

Ponieważ w każdym dokumencie w księdze gości używasz identyfikatora uwierzytelniania jako pola, możesz pobrać ten identyfikator i sprawdzić, czy każda osoba, która próbuje zapisać w dokumencie, ma zgodny identyfikator uwierzytelniania.

  1. 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;
    }
  }
}

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

  1. 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. Krok dodatkowy: ćwiczenie zdobytej wiedzy

Zapisywanie stanu odpowiedzi uczestnika

Obecnie Twoja aplikacja pozwala użytkownikom na prowadzenie czatu tylko wtedy, gdy są zainteresowani wydarzeniem. Ponadto jedynym sposobem, aby dowiedzieć się, czy ktoś się wybiera, jest zapytanie o to w 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 liczby uczestników.

  1. W pliku lib/app_state.dart dodaj te wiersze do sekcji ApplicationState w pliku ApplicationState, aby kod interfejsu użytkownika mógł wchodzić w interakcję 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});
  }
}
  1. Zaktualizuj metodę ApplicationState init() 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 zapytanie z ciągłym subskrypcją, 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 bierze udział w spotkaniu.

  1. U góry pliku lib/app_state.dart dodaj tę listę wartości.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Utwórz nowy plik yes_no_selection.dart i zdefiniuj nowy element, który będzie działał jak przyciski radiowe:

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 ma stan nieokreślony, w którym nie jest wybrana opcja Tak ani Nie. Gdy użytkownik wybierze, czy chce wziąć udział w wydarzeniu, możesz wyświetlić tę opcję jako przycisk z wypełnieniem, a drugą jako przycisk bez wypełnienia.

  1. Zaktualizuj metodę HomePage build(), aby korzystać z funkcji YesNoSelection, 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ą przycisków zostaną odrzucone. Musisz zaktualizować reguły, aby zezwolić na dodawanie elementów do kolekcji attendees.

  1. W kolekcji attendees pobierz identyfikator UID uwierzytelniania użyty jako nazwa dokumentu i sprawdź, czy uid osoby, która przesłała dokument, jest taki sam jak w dokumencie:
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 mogą odczytać listę uczestników, ponieważ nie ma na niej żadnych danych prywatnych, ale tylko twórca może ją aktualizować.

  1. 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 /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}
  1. Opcjonalnie: w aplikacji klikaj przyciski, aby wyświetlać wyniki w panelu Firestore w konsoli Firebase.

Podgląd aplikacji

Ekran główny aplikacji na Androidzie

Ekran główny aplikacji na iOS

Ekran główny aplikacji w wersji internetowej

Ekran główny aplikacji na macOS

10. Gratulacje!

Użyłaś/używałeś Firebase do utworzenia interaktywnej aplikacji internetowej działającej w czasie rzeczywistym.

Więcej informacji