Познакомьтесь с Firebase для Flutter

1. Прежде чем начать

В этом практическом занятии вы изучите основы Firebase для создания мобильных приложений Flutter для Android и iOS.

Предварительные требования

Что вы узнаете

  • Как создать приложение для подтверждения участия в мероприятии и чата с гостевой книгой на Android, iOS, в веб-версии и macOS с помощью Flutter.
  • Как аутентифицировать пользователей с помощью Firebase Authentication и синхронизировать данные с Firestore .

Главный экран приложения на Android

Главный экран приложения на iOS

Что вам понадобится

Любое из следующих устройств:

  • Физическое устройство Android или iOS, подключенное к компьютеру и настроенное на режим разработчика.
  • Симулятор iOS (требуются инструменты Xcode ).
  • Эмулятор Android (требуется настройка в Android Studio ).

Вам также потребуется следующее:

  • Любой браузер на ваш выбор, например, Google Chrome.
  • IDE или текстовый редактор на ваш выбор, настроенный с поддержкой плагинов Dart и Flutter, например Android Studio или Visual Studio Code .
  • Последняя stable версия Flutter или beta , если вам нравится рисковать.
  • Учетная запись Google для создания и управления вашим проектом Firebase.
  • Firebase CLI авторизован в вашем аккаунте Google.

2. Получите пример кода.

Загрузите начальную версию своего проекта с GitHub:

  1. В командной строке клонируйте репозиторий GitHub в каталог flutter-codelabs :
git clone https://github.com/flutter/codelabs.git flutter-codelabs

В каталоге flutter-codelabs находится код для набора практических заданий. Код для этого практического задания находится в каталоге flutter-codelabs/firebase-get-to-know-flutter . В этом каталоге содержится ряд снимков, показывающих, как должен выглядеть ваш проект в конце каждого шага. Например, вы находитесь на втором шаге.

  1. Найдите соответствующие файлы для второго шага:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Если вы хотите перейти к следующему шагу или посмотреть, как что-то должно выглядеть после его завершения, загляните в каталог, название которого соответствует интересующему вас шагу.

Импортируйте стартовое приложение

  • Откройте или импортируйте каталог flutter-codelabs/firebase-get-to-know-flutter/step_02 в вашей любимой IDE. Этот каталог содержит стартовый код для практического занятия, который представляет собой еще не работающее приложение для Flutter-встречи. При открытии приложения в IDE вы заметите ошибки компиляции, которые будут исправлены на шаге 4.

Найдите файлы, требующие обработки.

Код этого приложения разбросан по нескольким каталогам. Такое разделение функциональности упрощает работу, поскольку код группируется по функциональным параметрам.

  • Найдите следующие файлы:
    • lib/main.dart : Этот файл содержит основную точку входа и виджет приложения.
    • lib/home_page.dart : Этот файл содержит виджет домашней страницы.
    • lib/src/widgets.dart : Этот файл содержит несколько виджетов, помогающих стандартизировать стиль приложения. Они формируют экран стартового приложения.
    • lib/src/authentication.dart : Этот файл содержит частичную реализацию аутентификации с набором виджетов для создания пользовательского интерфейса входа в систему для аутентификации на основе электронной почты Firebase. Эти виджеты для процесса аутентификации пока не используются в стартовом приложении, но вы добавите их в ближайшее время.

Для сборки остальной части приложения вы добавляете дополнительные файлы по мере необходимости.

Просмотрите файл lib/main.dart

Это приложение использует пакет google_fonts , благодаря чему шрифт Roboto является шрифтом по умолчанию во всем приложении. Вы можете изучить fonts.google.com и использовать найденные там шрифты в разных частях приложения.

Вспомогательные виджеты из файла lib/src/widgets.dart представлены в виде элементов Header , Paragraph и IconAndDetail . Эти виджеты устраняют дублирование кода, уменьшая загромождение макета страницы, описанного в HomePage . Это также обеспечивает единообразный внешний вид.

Вот как ваше приложение выглядит на Android, iOS, в веб-версии и на macOS:

Главный экран приложения на Android

Главный экран приложения на iOS

Главный экран приложения в веб-версии

Главный экран приложения на macOS

3. Создайте и настройте проект Firebase.

Отображение информации о мероприятии отлично подходит для ваших гостей, но само по себе оно малополезно для кого-либо. Вам необходимо добавить в приложение динамическую функциональность. Для этого вам нужно подключить Firebase к вашему приложению. Чтобы начать работу с Firebase, вам нужно создать и настроить проект Firebase.

Создайте проект Firebase.

  1. Войдите в консоль Firebase, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например, Firebase-Flutter-Codelab ).
  3. Нажмите «Продолжить» .
  4. Если появится запрос, ознакомьтесь с условиями использования Firebase и примите их, после чего нажмите «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (в Firebase она называется "Gemini").
  6. Для этого практического занятия вам не понадобится Google Analytics, поэтому отключите эту опцию.
  7. Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .

Чтобы узнать больше о проектах Firebase, см. раздел «Понимание проектов Firebase» .

Настройка продуктов Firebase

В приложении используются следующие продукты Firebase, доступные для веб-приложений:

  • Аутентификация: позволяет пользователям входить в ваше приложение.
  • Firestore: Сохраняет структурированные данные в облаке и мгновенно уведомляет об изменениях данных.
  • Правила безопасности Firebase: обеспечивают безопасность вашей базы данных.

Для некоторых из этих продуктов требуется специальная настройка, или же их необходимо включить в консоли Firebase.

Включить аутентификацию по электронной почте

  1. В панели обзора проекта в консоли Firebase разверните меню «Сборка» .
  2. Нажмите «Аутентификация» > «Начать» > «Способ входа» > «Электронная почта/Пароль» > «Включить» > «Сохранить» .

На странице настроек аутентификации Firebase, с выбранной вкладкой «Способ входа», отображается, что поставщик «Электронная почта/Пароль» включен, а поставщик «Ссылка по электронной почте (вход без пароля)» отключен.

Настройте Firestore

Веб-приложение использует Firestore для сохранения сообщений чата и получения новых сообщений.

Вот как настроить Firestore в вашем проекте Firebase:

  1. В левой панели консоли Firebase разверните раздел «Сборка» , а затем выберите базу данных Firestore .
  2. Нажмите «Создать базу данных» .
  3. Выберите стандартную версию и нажмите «Далее».
  4. Оставьте значение параметра " Идентификатор базы данных" равным (default) .
  5. Выберите местоположение для вашей базы данных, затем нажмите «Далее» .
    Для создания настоящего приложения вам следует выбрать местоположение, расположенное недалеко от ваших пользователей.
  6. Нажмите «Пуск» в тестовом режиме . Ознакомьтесь с отказом от ответственности в отношении правил безопасности.
    В дальнейшем в этом практическом занятии вы добавите правила безопасности для защиты ваших данных. Не распространяйте и не предоставляйте публичный доступ к приложению, не добавив правила безопасности для вашей базы данных.
  7. Нажмите «Создать» .

4. Настройка Firebase

Для использования Firebase с Flutter необходимо выполнить следующие действия, чтобы настроить проект Flutter для корректного использования библиотек FlutterFire :

  1. Добавьте зависимости FlutterFire в свой проект.
  2. Зарегистрируйте выбранную платформу в проекте Firebase.
  3. Загрузите конфигурационный файл, специфичный для вашей платформы, и добавьте его в код.

В корневом каталоге вашего Flutter-приложения находятся подкаталоги android , ios , macos и web , в которых хранятся конфигурационные файлы для iOS и Android соответственно.

Настройка зависимостей

Вам необходимо добавить библиотеки FlutterFire для двух продуктов Firebase, которые вы используете в этом приложении: Authentication и Firestore.

  • В командной строке добавьте следующие зависимости из корневой папки вашего приложения ( .../firebase-get-to-know-flutter/step_02 ):
$ flutter pub add firebase_core firebase_auth cloud_firestore provider firebase_ui_auth

Для использования Firebase в вашем Flutter-приложении вам потребуется объединить несколько специализированных пакетов:

  • Пакет firebase_core : Это важнейшая отправная точка. Вам необходим этот пакет, поскольку все остальные инструменты Firebase для Flutter зависят от него.
  • Пакет firebase_auth : Этот пакет предназначен для управления учетными записями пользователей. Он позволяет добавлять такие функции, как регистрация, вход и выход из системы.
  • Пакет cloud_firestore : Используйте его для подключения вашего приложения к базе данных Firestore, что позволит вам сохранять и получать доступ к данным вашего приложения.
  • Пакет firebase_ui_auth : Этот пакет значительно ускоряет настройку аутентификации. Он предоставляет готовые к использованию виджеты (например, предварительно созданные экраны входа в систему), поэтому вам не нужно создавать все с нуля.
  • Пакет provider : это популярный выбор для управления состоянием. Он помогает вашему приложению отслеживать информацию (например, кто вошел в систему) и делать эти данные доступными на всех экранах, которым они необходимы.

Вы добавили необходимые пакеты, но вам также нужно настроить проекты iOS, Android, macOS и Web Runner для корректного использования Firebase. Кроме того, вы используете пакет provider , который обеспечивает разделение бизнес-логики и логики отображения.

Установите FlutterFire CLI.

Интерфейс командной строки FlutterFire зависит от базового интерфейса командной строки Firebase.

  1. Если вы еще этого не сделали, установите Firebase CLI на свой компьютер.
  2. Установите FlutterFire CLI:
$ dart pub global activate flutterfire_cli

После установки команда flutterfire становится доступна глобально.

Настройте свои приложения

Интерфейс командной строки извлекает информацию из вашего проекта Firebase и выбранных приложений проекта для генерации всей конфигурации для конкретной платформы.

В корневой директории вашего приложения ( flutter-codelabs/firebase-get-to-know-flutter/step_02 ) выполните команду configure :

$ flutterfire configure

Команда конфигурации выполняет следующие действия:

  1. Выберите проект Firebase на основе файла .firebaserc или из консоли Firebase.
  2. Определите платформы для настройки, такие как Android, iOS, macOS и веб-платформа.
  3. Определите приложения Firebase, из которых нужно извлечь конфигурацию. По умолчанию CLI пытается автоматически сопоставить приложения Firebase на основе текущей конфигурации вашего проекта.
  4. Создайте файл firebase_options.dart в вашем проекте.

Настройка macOS

Flutter на macOS позволяет создавать полностью изолированные приложения. Поскольку это приложение интегрируется с сетью для связи с серверами Firebase, вам необходимо настроить приложение с правами сетевого клиента.

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>

Для получения дополнительной информации см. раздел «Поддержка Flutter для настольных компьютеров» .

5. Добавить функцию подтверждения участия (RSVP).

Теперь, когда вы добавили Firebase в приложение, вы можете создать кнопку RSVP , которая регистрирует участников с помощью аутентификации . Для нативных приложений Android, iOS и веб-приложений существуют готовые пакеты FirebaseUI Auth , но для Flutter вам потребуется реализовать эту функцию самостоятельно.

Проект, который вы получили ранее, включал набор виджетов, реализующих пользовательский интерфейс для большей части процесса аутентификации. Вам необходимо реализовать бизнес-логику для интеграции аутентификации с приложением.

Добавьте бизнес-логику с помощью пакета Provider .

Используйте пакет provider , чтобы сделать централизованный объект состояния приложения доступным для всего дерева виджетов Flutter в приложении:

  1. Создайте новый файл с именем app_state.dart со следующим содержимым:

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();
    });
  }
}

Операторы import подключают Firebase Core и Auth, загружают пакет provider , который делает объект состояния приложения доступным во всем дереве виджетов, и включают виджеты аутентификации из пакета firebase_ui_auth .

Класс состояния приложения ApplicationState выполняет на этом этапе одну основную задачу: оповестить дерево виджетов о том, что произошло обновление состояния аутентификации.

Для передачи в приложение информации о состоянии авторизации пользователя используется только провайдер. Для авторизации пользователя используются интерфейсы, предоставляемые пакетом firebase_ui_auth , что является отличным способом быстрой инициализации экранов входа в систему в ваших приложениях.

Интегрируйте процесс аутентификации.

  1. Измените импорты в верхней части файла 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. Свяжите состояние приложения с инициализацией приложения, а затем добавьте поток аутентификации на HomePage :

lib/main.dart

void main() {
  // Modify from here...
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // ...to here.
}

Изменение функции main() делает пакет-провайдер ответственным за создание экземпляра объекта состояния приложения с виджетом ChangeNotifierProvider . Вы используете именно этот класс provider , потому что объект состояния приложения наследует класс ChangeNotifier , что позволяет пакету- provider знать, когда следует повторно отображать зависимые виджеты.

  1. Обновите свое приложение, чтобы оно обрабатывало навигацию по различным экранам, предоставляемым FirebaseUI , создав конфигурацию 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,
            ),
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity
      ),
      routerConfig: _router, // new
    );
  }
}

Для каждого экрана предусмотрено определенное действие в зависимости от нового состояния процесса аутентификации. После большинства изменений состояния аутентификации вы можете вернуться на предпочитаемый экран, будь то главный экран или другой экран, например, профиль.

  1. В методе build класса HomePage интегрируйте состояние приложения с виджетом 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!',
          ),
        ],
      ),
    );
  }
}

Вы создаете экземпляр виджета AuthFunc и оборачиваете его в виджет Consumer . Виджет Consumer — это обычный способ использования пакета provider для перестроения части дерева при изменении состояния приложения. Виджет AuthFunc — это дополнительные виджеты, которые вы тестируете.

Запустите приложение, чтобы протестировать процесс аутентификации.

cdf2d25e436bd48d.png

  1. В приложении нажмите кнопку RSVP , чтобы перейти на SignInScreen .

2a2cd6d69d172369.png

  1. Введите адрес электронной почты. Если вы уже зарегистрированы, система предложит вам ввести пароль. В противном случае система предложит вам заполнить регистрационную форму.

e5e65065dba36b54.png

  1. Введите пароль короче шести символов, чтобы проверить обработку ошибок. Если вы зарегистрированы, вместо этого вы увидите форму для ввода пароля.
  2. Введите неверный пароль, чтобы проверить алгоритм обработки ошибок.
  3. Введите правильный пароль. Вы увидите экран авторизации, который предлагает пользователю выйти из системы.

4ed811a25b0cf816.png

6. Запись сообщений в Firestore

Здорово, что пользователи приходят, но нужно предложить гостям что-то ещё для развлечения в приложении. Что если бы они могли оставлять сообщения в гостевой книге? Они могли бы рассказать, почему с нетерпением ждут приезда или с кем надеются встретиться.

Для хранения сообщений чата, которые пользователи пишут в приложении, используется Firestore .

модель данных

Firestore — это база данных NoSQL, и данные в ней разделены на коллекции, документы, поля и подколлекции. Каждое сообщение чата хранится как документ в коллекции guestbook , которая является коллекцией верхнего уровня.

7c20dc8424bb1d84.png

Добавить сообщения в Firestore

В этом разделе вы добавляете функциональность для отправки сообщений пользователями в базу данных. Сначала вы добавляете поле формы и кнопку отправки, а затем добавляете код, который связывает эти элементы с базой данных.

  1. Создайте новый файл с именем guest_book.dart , добавьте в него виджет GuestBook с сохранением состояния для построения элементов пользовательского интерфейса: поля для сообщения и кнопки отправки:

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

Здесь есть несколько интересных моментов. Во-первых, вы создаёте экземпляр формы, чтобы проверить, действительно ли сообщение содержит контент, и показать пользователю сообщение об ошибке, если его нет. Для проверки формы вы получаете доступ к её состоянию с помощью GlobalKey . Дополнительную информацию о ключах и способах их использования см. в разделе «Когда использовать ключи» .

Обратите внимание также на расположение виджетов: у вас есть Row с TextFormField и StyledButton , которая содержит Row . Также обратите внимание, что TextFormField заключено в виджет Expanded , который заставляет TextFormField заполнять любое дополнительное пространство в строке. Чтобы лучше понять, почему это необходимо, см. раздел «Понимание ограничений» .

Теперь, когда у вас есть виджет, позволяющий пользователю вводить текст для добавления в гостевую книгу, вам нужно отобразить его на экране.

  1. Отредактируйте содержимое элемента HomePage , добавив следующие две строки в конец дочерних элементов 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)),

Хотя этого достаточно для отображения виджета, этого недостаточно для выполнения каких-либо полезных действий. Вскоре вам потребуется обновить этот код, чтобы он заработал.

Предварительный просмотр приложения

Главный экран приложения на Android с интеграцией чата.

Главный экран приложения на iOS с интеграцией чата.

Главный экран веб-версии приложения с интеграцией чата.

Главный экран приложения на macOS с интеграцией чата.

Когда пользователь нажимает кнопку «ОТПРАВИТЬ» , запускается следующий фрагмент кода. Он добавляет содержимое поля ввода сообщения в коллекцию guestbook базы данных. В частности, метод addMessageToGuestBook добавляет содержимое сообщения в новый документ с автоматически сгенерированным идентификатором в коллекции guestbook .

Обратите внимание, что FirebaseAuth.instance.currentUser.uid — это ссылка на автоматически сгенерированный уникальный идентификатор, который аутентификация присваивает всем авторизованным пользователям.

  • В файл lib/app_state.dart добавьте метод addMessageToGuestBook . На следующем шаге вы свяжете эту возможность с пользовательским интерфейсом.

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

Соедините пользовательский интерфейс и базу данных.

У вас есть пользовательский интерфейс, где пользователь может ввести текст, который он хочет добавить в гостевую книгу, и код для добавления записи в Firestore. Теперь вам нужно только соединить эти два элемента.

  • В файле lib/home_page.dart внесите следующие изменения в виджет 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({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'),
          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.
        ],
      ),
    );
  }
}

Вы заменили две строки, добавленные в начале этого шага, полной реализацией. Вы снова используете Consumer<ApplicationState> , чтобы сделать состояние приложения доступным для той части дерева, которую вы отображаете. Это позволяет вам реагировать на сообщения, которые кто-то вводит в пользовательский интерфейс, и публиковать их в базе данных. В следующем разделе вы проверяете, опубликованы ли добавленные сообщения в базе данных.

Проверка отправки сообщений

  1. При необходимости войдите в приложение.
  2. Введите сообщение, например, Hey there! , а затем нажмите «ОТПРАВИТЬ» .

Это действие записывает сообщение в вашу базу данных Firestore. Однако вы не увидите сообщение в самом приложении Flutter, поскольку вам еще нужно реализовать получение данных, что вы сделаете на следующем шаге. Тем не менее, на панели управления базами данных в консоли Firebase вы можете увидеть добавленное сообщение в коллекции guestbook . Если вы отправляете больше сообщений, вы добавляете больше документов в свою коллекцию guestbook . Например, см. следующий фрагмент кода:

713870af0b3b63c.png

7. Читайте сообщения

Замечательно, что гости могут оставлять сообщения в базе данных, но пока они не видят их в приложении. Пора это исправить!

Синхронизация сообщений

Для отображения сообщений необходимо добавить обработчики событий, которые срабатывают при изменении данных, а затем создать элемент пользовательского интерфейса, отображающий новые сообщения. В состояние приложения нужно добавить код, который будет отслеживать появление новых сообщений.

  1. Создайте новый файл guest_book_message.dart и добавьте следующий класс, чтобы отобразить структурированное представление данных, которые вы храните в Firestore.

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. В файл lib/app_state.dart добавьте следующие импорты:

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. В разделе ApplicationState , где вы определяете состояние и геттеры, добавьте следующие строки:

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. В разделе инициализации объекта ApplicationState добавьте следующие строки, чтобы подписаться на запрос к коллекции документов при входе пользователя в систему и отписаться при выходе из нее:

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;
        // Add from here...
        _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();
        });
        // ...to here.
      } else {
        _loggedIn = false;
        // Add from here...
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

Этот раздел важен, потому что именно здесь вы формируете запрос к коллекции guestbook и обрабатываете подписку и отписку от этой коллекции. Вы прослушиваете поток, где восстанавливаете локальный кэш сообщений из коллекции guestbook , а также сохраняете ссылку на эту подписку, чтобы позже отписаться от неё. Здесь происходит много всего, поэтому вам следует изучить это в отладчике, чтобы понять, что происходит, и получить более ясную модель. Для получения дополнительной информации см. раздел «Получение обновлений в реальном времени с помощью Firestore» .

  1. В файл lib/guest_book.dart добавьте следующий импорт:
import 'guest_book_message.dart';
  1. В виджете GuestBook добавьте список сообщений в качестве части конфигурации, чтобы связать это изменяющееся состояние с пользовательским интерфейсом:

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
  State<GuestBook> createState() => _GuestBookState();
}
  1. В _GuestBookState измените метод build следующим образом, чтобы предоставить доступ к этой конфигурации:

lib/guest_book.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    // Modify from here...
    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: [
                      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.
    );
  }
}

Вы оборачиваете предыдущее содержимое метода build() виджетом Column , а затем добавляете коллекцию в конец дочерних элементов Column , чтобы сгенерировать новый Paragraph для каждого сообщения в списке сообщений.

  1. Обновите тело объекта HomePage , чтобы корректно сформировать GuestBook с новым параметром 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
        ),
      ],
    ],
  ),
),

Проверка синхронизации сообщений

Firestore автоматически и мгновенно синхронизирует данные с клиентами, подписанными на базу данных.

Проверка синхронизации сообщений:

  1. В приложении найдите в базе данных сообщения, которые вы создали ранее.
  2. Пишите новые сообщения. Они появляются мгновенно.
  3. Откройте рабочее пространство в нескольких окнах или вкладках. Сообщения синхронизируются в режиме реального времени между окнами и вкладками.
  4. Дополнительно: В меню «База данных» консоли Firebase можно вручную удалять, изменять или добавлять новые сообщения. Все изменения отображаются в пользовательском интерфейсе.

Поздравляем! Вы прочитали документы Firestore в своем приложении!

Предварительный просмотр приложения

Главный экран приложения на Android с интеграцией чата.

Главный экран приложения на iOS с интеграцией чата.

Главный экран веб-версии приложения с интеграцией чата.

Главный экран приложения на macOS с интеграцией чата.

8. Настройте основные правила безопасности.

Изначально вы настроили Firestore на использование тестового режима, что означает, что ваша база данных открыта для чтения и записи. Однако тестовый режим следует использовать только на ранних этапах разработки. В качестве лучшей практики следует настраивать правила безопасности для вашей базы данных по мере разработки приложения. Безопасность является неотъемлемой частью структуры и поведения вашего приложения.

Правила безопасности Firebase позволяют контролировать доступ к документам и коллекциям в вашей базе данных. Гибкий синтаксис правил позволяет создавать правила, которые соответствуют чему угодно — от всех операций записи во всю базу данных до операций над конкретным документом.

Настройте основные правила безопасности:

  1. В меню «Разработка» консоли Firebase выберите «База данных» > «Правила» . Вы должны увидеть следующие правила безопасности по умолчанию и предупреждение о том, что эти правила являются общедоступными:

7767a2d2e64e7275.png

  1. Укажите коллекции, в которые приложение записывает данные:

В match /databases/{database}/documents укажите коллекцию, которую вы хотите защитить:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Поскольку вы использовали UID аутентификации в качестве поля в каждом документе гостевой книги, вы можете получить UID аутентификации и убедиться, что у любого, кто пытается записать данные в документ, совпадает UID аутентификации.

  1. Добавьте правила чтения и записи в свой набор правил:
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;
    }
  }
}

Теперь только авторизованные пользователи могут читать сообщения в гостевой книге, но редактировать сообщение может только его автор.

  1. Добавьте проверку данных, чтобы убедиться, что в документе присутствуют все необходимые поля:
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. Дополнительный шаг: Примените полученные знания на практике.

Зафиксируйте статус подтверждения участия участника.

В настоящий момент ваше приложение позволяет пользователям общаться в чате только тогда, когда они заинтересованы в мероприятии. Кроме того, узнать о том, что кто-то придёт, можно только после того, как он сам сообщит об этом в чате.

На этом этапе вы организуете мероприятие и сообщаете людям, сколько человек придет. Вы добавляете несколько функций в состояние приложения. Первая — это возможность для авторизованного пользователя указать, будет ли он присутствовать. Вторая — это счетчик количества участников.

  1. В файле lib/app_state.dart добавьте следующие строки в раздел accessors объекта ApplicationState , чтобы код пользовательского интерфейса мог взаимодействовать с этим состоянием:

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. Обновите метод init() объекта ApplicationState следующим образом:

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();
    });
  }

Этот код добавляет постоянно активный запрос для определения количества участников и второй запрос, который активен только тогда, когда пользователь авторизован, для определения того, участвует ли пользователь в мероприятии.

  1. Добавьте следующее перечисление в начало файла lib/app_state.dart .

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Создайте новый файл yes_no_selection.dart и определите новый виджет, который будет работать как переключатели:

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

Изначально всё находится в неопределённом состоянии, без выбора «Да» или «Нет» . После того, как пользователь выберет, будет ли он присутствовать, этот вариант отображается выделенной кнопкой, а другой вариант скрывается, оставаясь без предварительного выбора.

  1. Обновите метод build() класса HomePage , чтобы он использовал преимущества YesNoSelection , дайте авторизованному пользователю возможность указать, будет ли он присутствовать, и отобразите количество участников мероприятия:

lib/home_page.dart

import 'yes_no_selection.dart';             // new

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

Добавить правила

Вы уже настроили некоторые правила, поэтому данные, которые вы добавляете с помощью кнопок, будут отклонены. Вам необходимо обновить правила, чтобы разрешить добавление attendees в коллекцию.

  1. В коллекции attendees найдите UID аутентификации, который вы использовали в качестве имени документа, и убедитесь, что uid отправителя совпадает с UID документа, который он пишет:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Это позволяет всем желающим ознакомиться со списком участников, поскольку в нем нет конфиденциальных данных, и только создатель мероприятия может его обновлять.

  1. Добавьте проверку данных, чтобы убедиться, что в документе присутствуют все необходимые поля:
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. Дополнительно: В приложении нажмите кнопки, чтобы просмотреть результаты на панели мониторинга Firestore в консоли Firebase.

Предварительный просмотр приложения

Главный экран приложения на Android

Главный экран приложения на iOS

Главный экран приложения в веб-версии

Главный экран приложения на macOS

10. Поздравляем!

Вы использовали Firebase для создания интерактивного веб-приложения, работающего в режиме реального времени!

Узнать больше