Познакомьтесь с 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 вошел в вашу учетную запись Google.

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

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

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

Каталог flutter-codelabs содержит код для коллекции 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.

Найдите файлы, над которыми нужно работать

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

  • Найдите следующие файлы:
    • 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 .
  2. В консоли нажмите «Добавить проект» или «Создать проект» .
  3. В поле «Имя проекта» введите Firebase-Flutter-Codelab и нажмите «Продолжить» .

4395e4e67c08043a.png

  1. Просмотрите параметры создания проекта. При появлении запроса примите условия Firebase, но пропустите настройку Google Analytics, поскольку вы не будете использовать его для этого приложения.

b7138cde5f2c7b61.png

Дополнительные сведения о проектах Firebase см. в разделе Общие сведения о проектах Firebase .

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

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

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

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

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

58e3e3e23c2f16a4.png

Настроить Firestore

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

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

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

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

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

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

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

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

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

  • В командной строке добавьте следующие зависимости:
$ flutter pub add firebase_core

Пакет firebase_core — это общий код, необходимый для всех плагинов Firebase Flutter.

$ flutter pub add firebase_auth

Пакет firebase_auth обеспечивает интеграцию с аутентификацией.

$ flutter pub add cloud_firestore

Пакет cloud_firestore обеспечивает доступ к хранилищу данных Firestore.

$ flutter pub add provider

Пакет firebase_ui_auth предоставляет набор виджетов и утилит для увеличения скорости разработки с помощью потоков аутентификации.

$ flutter pub add firebase_ui_auth

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

Установите интерфейс командной строки FlutterFire.

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

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

После установки команда flutterfire доступна по всему миру.

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

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

В корне вашего приложения выполните команду 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 :

библиотека/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 :

библиотека/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 :

библиотека/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
    );
  }
}

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

  1. В методе сборки класса 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. В приложении нажмите кнопку «Ответить» , чтобы запустить 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({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.
        ],
      ),
    );
  }
}

Вы заменили две строки, добавленные в начале этого шага, полной реализацией. Вы снова используете 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;
        _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();
    });
  }

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

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

Вы оборачиваете предыдущее содержимое метода build() виджетом Column , а затем добавляете коллекцию for в хвост дочерних элементов 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 добавьте следующие строки в раздел средств доступа 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

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 отправителя совпадает с идентификатором документа, который он пишет:
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 для создания интерактивного веб-приложения, работающего в реальном времени!

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