Знакомство с Firebase для Flutter

Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.

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

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

Предпосылки

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

Что вы создадите

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

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

Вы можете запустить эту лабораторию кода, используя любое из следующих устройств:

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

Помимо вышеперечисленного, вам также понадобятся:

  • Браузер на ваш выбор, например Chrome.
  • IDE или текстовый редактор по вашему выбору, например Android Studio или VS Code , настроенный с подключаемыми модулями Dart и Flutter.
  • Последняя stable версия Flutter (или beta , если вам нравится жить на грани).
  • Учетная запись Google, например учетная запись Gmail, для создания и управления вашим проектом Firebase.
  • Инструмент командной строки firebase , вошедший в вашу учетную запись gmail.
  • Пример кода лаборатории кода. См. следующий шаг, чтобы узнать, как получить код.

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

Начнем со скачивания начальной версии нашего проекта с GitHub.

Клонируйте репозиторий GitHub из командной строки:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

В качестве альтернативы, если у вас установлен инструмент GitHub cli :

gh repo clone flutter/codelabs flutter-codelabs

Образец кода должен быть клонирован в каталог flutter-codelabs , который содержит код для коллекции codelabs. Код для этой кодлабы находится в flutter-codelabs/firebase-get-to-know-flutter .

Структура каталогов под flutter-codelabs/firebase-get-to-know-flutter представляет собой серию снимков того, где вы должны находиться в конце каждого именованного шага. Это шаг 2, поэтому найти соответствующие файлы так же просто, как:

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

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

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

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

Найдите файлы для работы

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

Найдите в проекте следующие файлы:

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

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

Проверка файла lib/main.dart

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

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

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

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

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

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

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

  1. Войдите в Firebase .
  2. В консоли Firebase нажмите «Добавить проект» (или «Создать проект» ) и назовите свой проект Firebase Firebase-Flutter-Codelab .

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

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

  • Firebase Authentication , чтобы ваши пользователи могли входить в ваше приложение.
  • Cloud Firestore для сохранения структурированных данных в облаке и получения мгновенных уведомлений при изменении данных.
  • Правила безопасности Firebase для защиты вашей базы данных.

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

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

Чтобы разрешить пользователям входить в веб-приложение, вы будете использовать метод входа по электронной почте/паролю для этой лаборатории кода:

  1. В консоли Firebase разверните меню « Сборка » на левой панели.
  2. Нажмите « Аутентификация » , затем нажмите кнопку « Начать », затем вкладку « Метод входа » (или щелкните здесь, чтобы перейти непосредственно на вкладку « Метод входа »).
  3. Нажмите « Электронная почта/пароль » в списке « Поставщики входа », установите переключатель « Включить » в положение «Вкл.», а затем нажмите « Сохранить » . 58e3e3e23c2f16a4.png

Включить облачное хранилище Firestore

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

Включить облачное хранилище Firestore:

  1. В разделе « Сборка » консоли Firebase нажмите « Cloud Firestore ».
  2. Щелкните Создать базу данных . 99e8429832d23fa3.png
  1. Выберите опцию Запустить в тестовом режиме . Прочитайте отказ от ответственности о правилах безопасности. Тестовый режим гарантирует, что вы можете свободно писать в базу данных во время разработки. Нажмите «Далее» . 6be00e26c72ea032.png
  1. Выберите место для вашей базы данных (вы можете просто использовать значение по умолчанию). Обратите внимание, что это местоположение нельзя будет изменить позже. 278656eefcfb0216.png
  2. Щелкните Включить .

4. Конфигурация базы данных

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

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

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

Настроить зависимости

Вам необходимо добавить библиотеки FlutterFire для двух продуктов Firebase, которые вы используете в этом приложении — Firebase Auth и Cloud Firestore. Выполните следующие три команды, чтобы добавить зависимости.

$ flutter pub add firebase_core

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

$ flutter pub add firebase_auth

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

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

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

$ flutter pub add firebase_ui_auth

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

Установка flutterfire

Интерфейс командной строки FlutterFire зависит от базового интерфейса командной строки Firebase. Если вы еще этого не сделали, убедитесь, что Firebase CLI установлен на вашем компьютере.

Затем установите интерфейс командной строки FlutterFire, выполнив следующую команду:

$ dart pub global activate flutterfire_cli

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

Настройка ваших приложений

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

В корне вашего приложения запустите команду configure:

$ flutterfire configure

Команда конфигурации проведет вас через ряд процессов:

  1. Выбор проекта Firebase (на основе файла .firebaserc или из консоли Firebase).
  2. Подскажите, для каких платформ (например, Android, iOS, macOS и веб) вам нужна конфигурация.
  3. Определите, какие приложения Firebase для выбранных платформ следует использовать для извлечения конфигурации. По умолчанию интерфейс командной строки попытается автоматически сопоставить приложения 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>

Дополнительные сведения см. в разделе Права и песочница приложения .

5. Добавьте вход пользователя (RSVP)

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

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

Бизнес-логика с провайдером

Вы собираетесь использовать пакет provider , чтобы сделать объект централизованного состояния приложения доступным в дереве виджетов Flutter приложения. Для начала измените импорт в верхней части lib/main.dart :

библиотека/main.dart

import 'dart:async';                                     // new
import 'package:firebase_auth/firebase_auth.dart'        // new
    hide EmailAuthProvider, PhoneAuthProvider;           // new
import 'package:firebase_core/firebase_core.dart';       // new
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'firebase_options.dart';                          // new
import 'src/authentication.dart';                        // new
import 'src/widgets.dart';

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

Этот объект состояния приложения, ApplicationState , имеет одну основную обязанность на этом шаге, которая заключается в предупреждении дерева виджетов о том, что произошло обновление до состояния аутентификации. Добавьте следующий класс в конец lib/main.dart :

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

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

Интеграция потока аутентификации

Теперь, когда у вас есть начало состояния приложения, пришло время связать состояние приложения с инициализацией приложения и добавить поток проверки подлинности в HomePage . Обновите основную точку входа, чтобы интегрировать состояние приложения через пакет provider :

библиотека/main.dart

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

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

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

Поскольку мы используем FirebaseUI для Flutter, мы собираемся обновить наше приложение, чтобы оно могло обрабатывать переходы к различным экранам, которые предоставляет нам FirebaseUI. Для этого мы добавляем свойство initialRoute и добавляем наши предпочтительные экраны, на которые мы можем перенаправляться в свойстве routes . Изменения должны выглядеть так:

библиотека/main.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //Start adding here
      initialRoute: '/home',
      routes: {
        '/home': (context) {
          return const HomePage();
        },
        '/sign-in': ((context) {
          return SignInScreen(
            actions: [
              ForgotPasswordAction(((context, email) {
                Navigator.of(context)
                    .pushNamed('/forgot-password', arguments: {'email': email});
              })),
              AuthStateChangeAction(((context, state) {
                if (state is SignedIn || state is UserCreated) {
                  var user = (state is SignedIn)
                      ? state.user
                      : (state as UserCreated).credential.user;
                  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);
                  }
                  Navigator.of(context).pushReplacementNamed('/home');
                }
              })),
            ],
          );
        }),
        '/forgot-password': ((context) {
          final arguments = ModalRoute.of(context)?.settings.arguments
              as Map<String, dynamic>?;

          return ForgotPasswordScreen(
            email: arguments?['email'] as String,
            headerMaxExtent: 200,
          );
        }),
        '/profile': ((context) {
          return ProfileScreen(
            providers: [],
            actions: [
              SignedOutAction(
                ((context) {
                  Navigator.of(context).pushReplacementNamed('/home');
                }),
              ),
            ],
          );
        })
      },
      // end adding here
      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,
      ),
    );
  }
}

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

библиотека/main.dart

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
        builder: (context, appState, child) => 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

Вот начало процесса аутентификации, когда пользователь может нажать на кнопку RSVP, чтобы инициировать SignInScreen .

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

Обязательно попробуйте ввести короткий пароль (менее шести символов), чтобы проверить процесс обработки ошибок. Если пользователь зарегистрирован, вместо него он увидит пароль.

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

4ed811a25b0cf816.png

И с этим вы внедрили поток аутентификации. Поздравляю!

6. Пишите сообщения в Cloud Firestore

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

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

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

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

7c20dc8424bb1d84.png

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

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

Сначала добавьте импорт для пакета cloud_firestore и dart:async .

библиотека/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

Чтобы создать элементы пользовательского интерфейса для поля сообщения и кнопки отправки, добавьте новый виджет GuestBook с отслеживанием состояния в нижней части lib/main.dart .

библиотека/main.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 . Для получения дополнительной информации о ключах и о том, как их использовать, см. эпизод Flutter Widgets 101 «Когда использовать ключи» .

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

Теперь, когда у вас есть виджет, который позволяет пользователю вводить текст для добавления в гостевую книгу, вам нужно вывести его на экран. Для этого отредактируйте тело 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)),

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

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

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

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

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

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

Подключение пользовательского интерфейса к базе данных

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

библиотека/main.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. Введите сообщение, например "Привет!", а затем нажмите ОТПРАВИТЬ .

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

Но вы можете увидеть только что добавленное сообщение в консоли Firebase.

В консоли Firebase на панели управления базой данных вы должны увидеть коллекцию guestbook с добавленным вами сообщением. Если вы продолжите отправлять сообщения, ваша коллекция гостевой книги будет содержать много документов, например:

Консоль Firebase

713870af0b3b63c.png

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

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

Синхронизировать сообщения

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

Чуть выше виджета GuestBook книги находится следующий класс значений. Этот класс предоставляет структурированное представление данных, которые вы храните в Cloud Firestore.

библиотека/main.dart

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

В разделе ApplicationState , где вы определяете состояние и геттеры, добавьте следующие новые строки:

библиотека/main.dart

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

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

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

библиотека/main.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 книги, а также сохраняете ссылку на эту подписку, чтобы потом от нее можно было отписаться. Здесь много чего происходит, и стоит потратить некоторое время на отладчик, проверяя, что происходит, когда получить более четкую ментальную модель.

Для получения дополнительной информации см. документацию Cloud Firestore .

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

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

Затем мы раскрываем эту новую конфигурацию в _GuestBookState , изменяя метод build следующим образом.

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

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

Наконец, теперь вам нужно обновить тело HomePage , чтобы правильно построить GuestBook с новым параметром messages .

библиотека/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

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

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

  1. Сообщения, созданные ранее в базе данных, должны отображаться в приложении. Не стесняйтесь писать новые сообщения; они должны появиться мгновенно.
  2. Если вы откроете свое рабочее пространство в нескольких окнах или вкладках, сообщения будут синхронизироваться в режиме реального времени между вкладками.
  3. (Необязательно) Вы можете попробовать вручную удалить, изменить или добавить новые сообщения непосредственно в разделе « База данных » консоли Firebase; любые изменения должны появиться в пользовательском интерфейсе.

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

Обзор приложения

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

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

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

Вы можете написать правила безопасности для Cloud Firestore в консоли Firebase:

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

7767a2d2e64e7275.png

Определить коллекции

Сначала определите коллекции, в которые приложение записывает данные.

В 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 аутентификации.

Добавьте правила чтения и записи в свой набор правил, как показано ниже:

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

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

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

Добавьте проверку данных, чтобы убедиться, что все ожидаемые поля присутствуют в документе:

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. Бонусный шаг: Практика того, что вы узнали

Запись статуса RSVP участника

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

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

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

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

Обновите метод init ApplicationState следующим образом:

библиотека/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Вышеупомянутый добавляет всегда подписанный запрос, чтобы узнать количество посетителей, и второй запрос, который активен только тогда, когда пользователь вошел в систему, чтобы узнать, посещает ли пользователь. Затем добавьте следующее перечисление после объявления GuestBookMessage :

библиотека/main.dart

enum Attending { yes, no, unknown }

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

библиотека/main.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: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                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),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                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'),
              ),
            ],
          ),
        );
    }
  }
}

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

библиотека/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        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 .

Для коллекции 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;
    }
  }
}

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

Добавьте проверку данных, чтобы убедиться, что все ожидаемые поля присутствуют в документе:

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;

    }
  }
}

(Необязательно) Теперь вы можете просматривать результаты нажатия кнопок. Перейдите на панель инструментов Cloud Firestore в консоли Firebase.

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

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

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

Что мы рассмотрели

  • Аутентификация Firebase
  • Облако Firestore
  • Правила безопасности Firebase

Следующие шаги

  • Хотите узнать больше о других продуктах Firebase? Может быть, вы хотите хранить файлы изображений, которые загружают пользователи? Или отправлять уведомления своим пользователям? Ознакомьтесь с документацией Firebase . Хотите узнать больше о плагинах Flutter для Firebase? Посетите FlutterFire для получения дополнительной информации.
  • Хотите узнать больше о Cloud Firestore? Может быть, вы хотите узнать о подколлекциях и транзакциях? Перейдите в веб-лабораторию кода Cloud Firestore, чтобы получить более подробную информацию о Cloud Firestore. Или посмотрите эту серию YouTube, чтобы познакомиться с Cloud Firestore !

Учить больше

Как прошло?

Будем рады вашему отзыву! Пожалуйста, заполните (очень) короткую форму здесь .