1. Прежде чем начать
В этом практикуме вы изучите основы Firebase для создания мобильных приложений Flutter для Android и iOS.
Предпосылки
- Знакомство с Flutter
- Flutter SDK
- Текстовый редактор по вашему выбору
Чему вы научитесь
- Как создать приложение для RSVP-подтверждения участия и чат-книгу для мероприятий на Android, iOS, в Интернете и macOS с помощью Flutter.
- Как аутентифицировать пользователей с помощью аутентификации Firebase и синхронизировать данные с Firestore.
Что вам понадобится
Любое из следующих устройств:
- Физическое устройство Android или iOS, подключенное к компьютеру и настроенное на режим разработчика.
- Симулятор iOS (требуются инструменты Xcode ).
- Эмулятор Android (требует настройки в Android Studio ).
Вам также понадобится следующее:
- Браузер по вашему выбору, например Google Chrome.
- IDE или текстовый редактор по вашему выбору, настроенный с плагинами Dart и Flutter, например Android Studio или Visual Studio Code .
- Последняя
stable
версия Flutter илиbeta
если вам нравится быть на грани. - Учетная запись Google для создания и управления вашим проектом Firebase.
-
Firebase
CLI вошел в вашу учетную запись Google.
2. Получите пример кода
Загрузите начальную версию вашего проекта с GitHub:
- Из командной строки клонируйте репозиторий GitHub в каталог
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Каталог flutter-codelabs
содержит код для набора практических заданий. Код этого задания находится в каталоге flutter-codelabs/firebase-get-to-know-flutter
. Этот каталог содержит серию снимков, показывающих, как должен выглядеть ваш проект в конце каждого этапа. Например, вы находитесь на втором этапе.
- Найдите соответствующие файлы для второго шага:
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/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:
3. Создайте и настройте проект Firebase.
Отображение информации о мероприятии отлично подходит для ваших гостей, но само по себе малополезно для кого-либо. Вам необходимо добавить в приложение динамическую функциональность. Для этого необходимо подключить Firebase к приложению. Чтобы начать работу с Firebase, необходимо создать и настроить проект Firebase.
Создать проект Firebase
- Войдите в консоль Firebase, используя свою учетную запись Google.
- Нажмите кнопку, чтобы создать новый проект, а затем введите имя проекта (например,
Firebase-Flutter-Codelab
). - Нажмите «Продолжить» .
- При появлении соответствующего запроса ознакомьтесь с условиями Firebase и примите их, а затем нажмите кнопку «Продолжить» .
- (Необязательно) Включите помощь ИИ в консоли Firebase (так называемая «Gemini в Firebase»).
- Для этой лабораторной работы вам не понадобится Google Analytics, поэтому отключите опцию Google Analytics.
- Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .
Дополнительную информацию о проектах Firebase см. в разделе Понимание проектов Firebase .
Настройка продуктов Firebase
Приложение использует следующие продукты Firebase, доступные для веб-приложений:
- Аутентификация: позволяет пользователям входить в ваше приложение.
- Firestore: сохраняет структурированные данные в облаке и получает мгновенные уведомления об изменении данных.
- Правила безопасности Firebase: защищают вашу базу данных.
Некоторые из этих продуктов требуют специальной настройки или их необходимо включить в консоли Firebase.
Включить аутентификацию при входе по электронной почте
- На панели обзора проекта консоли Firebase разверните меню «Сборка» .
- Нажмите «Аутентификация» > «Начать» > «Способ входа» > «Электронная почта/пароль» > «Включить» > «Сохранить» .
Настройка Firestore
Веб-приложение использует Firestore для сохранения сообщений чата и получения новых сообщений чата.
Вот как настроить Firestore в вашем проекте Firebase:
- На левой панели консоли Firebase разверните пункт «Сборка» , а затем выберите «База данных Firestore» .
- Нажмите Создать базу данных .
- Оставьте идентификатор базы данных равным
(default)
. - Выберите местоположение вашей базы данных, затем нажмите «Далее» .
Для настоящего приложения вам нужно выбрать местоположение, близкое к вашим пользователям. - Нажмите «Начать в тестовом режиме» . Ознакомьтесь с отказом от ответственности о правилах безопасности.
Далее в этой лабораторной работе вы добавите правила безопасности для защиты своих данных. Не распространяйте и не публикуйте приложение, не добавив правила безопасности для своей базы данных. - Нажмите «Создать» .
4. Настройте Firebase
Чтобы использовать Firebase с Flutter, вам необходимо выполнить следующие задачи, чтобы настроить проект Flutter для правильного использования библиотек FlutterFire
:
- Добавьте зависимости
FlutterFire
в свой проект. - Зарегистрируйте нужную платформу в проекте Firebase.
- Загрузите файл конфигурации для конкретной платформы, а затем добавьте его в код.
В каталоге верхнего уровня вашего приложения 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 CLI
Интерфейс командной строки FlutterFire зависит от базового интерфейса командной строки Firebase.
- Если вы еще этого не сделали, установите Firebase CLI на свой компьютер.
- Установите FlutterFire CLI:
$ dart pub global activate flutterfire_cli
После установки команда flutterfire
станет доступна по всему миру.
Настройте свои приложения
CLI извлекает информацию из вашего проекта Firebase и выбранных приложений проекта, чтобы сгенерировать всю конфигурацию для конкретной платформы.
В корне вашего приложения выполните команду configure
:
$ flutterfire configure
Команда конфигурации проведет вас через следующие процессы:
- Выберите проект Firebase на основе файла
.firebaserc
или из консоли Firebase. - Определите платформы для конфигурации, такие как Android, iOS, macOS и веб.
- Определите, из каких приложений Firebase нужно извлечь конфигурацию. По умолчанию CLI автоматически подбирает приложения Firebase на основе текущей конфигурации проекта.
- Создайте файл
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 , которая будет регистрировать пользователей с помощью Authentication . Для Android, iOS и веб-приложений существуют готовые пакеты FirebaseUI Auth
, но для Flutter эту возможность необходимо реализовать.
Проект, который вы получили ранее, включал набор виджетов, реализующих пользовательский интерфейс для большей части процесса аутентификации. Вы реализуете бизнес-логику для интеграции аутентификации с приложением.
Добавьте бизнес-логику с помощью пакета Provider
Используйте пакет provider
, чтобы сделать централизованный объект состояния приложения доступным во всем дереве виджетов Flutter приложения:
- Создайте новый файл с именем
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
, что является отличным способом быстрой загрузки экранов входа в приложения.
Интеграция процесса аутентификации
- Измените импорт в верхней части файла
lib/main.dart
:
lib/main.dart
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'home_page.dart';
- Свяжите состояние приложения с инициализацией приложения, а затем добавьте поток аутентификации на
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
Изменение функции main()
возлагает на пакет поставщика ответственность за создание экземпляра объекта состояния приложения с виджетом ChangeNotifierProvider
. Вы используете этот класс provider
, поскольку объект состояния приложения расширяет класс ChangeNotifier
, который сообщает пакету provider
, когда следует повторно отображать зависимые виджеты.
- Обновите свое приложение, чтобы оно могло обрабатывать навигацию по различным экранам, предоставляемым FirebaseUI, создав конфигурацию
GoRouter
:
lib/main.dart
// Add GoRouter configuration outside the App class
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'sign-in',
builder: (context, state) {
return SignInScreen(
actions: [
ForgotPasswordAction(((context, email) {
final uri = Uri(
path: '/sign-in/forgot-password',
queryParameters: <String, String?>{
'email': email,
},
);
context.push(uri.toString());
})),
AuthStateChangeAction(((context, state) {
final user = switch (state) {
SignedIn state => state.user,
UserCreated state => state.credential.user,
_ => null
};
if (user == null) {
return;
}
if (state is UserCreated) {
user.updateDisplayName(user.email!.split('@')[0]);
}
if (!user.emailVerified) {
user.sendEmailVerification();
const snackBar = SnackBar(
content: Text(
'Please check your email to verify your email address'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
context.pushReplacement('/');
})),
],
);
},
routes: [
GoRoute(
path: 'forgot-password',
builder: (context, state) {
final arguments = state.uri.queryParameters;
return ForgotPasswordScreen(
email: arguments['email'],
headerMaxExtent: 200,
);
},
),
],
),
GoRoute(
path: 'profile',
builder: (context, state) {
return ProfileScreen(
providers: const [],
actions: [
SignedOutAction((context) {
context.pushReplacement('/');
}),
],
);
},
),
],
),
],
);
// end of GoRouter configuration
// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
routerConfig: _router, // new
);
}
}
С каждым экраном связан свой тип действия, зависящий от нового состояния процесса аутентификации. После большинства изменений состояния аутентификации вы можете вернуться на предпочитаемый экран, будь то главный экран или другой экран, например, профиль.
- В методе сборки класса
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
— это дополнительные виджеты, которые вы тестируете.
Протестируйте процесс аутентификации
- В приложении нажмите кнопку RSVP , чтобы инициировать
SignInScreen
.
- Введите адрес электронной почты. Если вы уже зарегистрированы, система предложит вам ввести пароль. В противном случае система предложит вам заполнить регистрационную форму.
- Введите пароль длиной менее шести символов, чтобы проверить обработку ошибок. Если вы зарегистрированы, вместо этого вы увидите пароль.
- Введите неверные пароли, чтобы проверить процесс обработки ошибок.
- Введите правильный пароль. Вы увидите окно входа в систему, которое предлагает пользователю возможность выйти из системы.
6. Напишите сообщения в Firestore
Приятно знать, что пользователи приходят, но нужно дать гостям что-то ещё, чем можно заняться в приложении. Что, если бы они могли оставлять сообщения в гостевой книге? Они могли бы рассказать, почему они рады прийти или с кем надеются встретиться.
Для хранения сообщений чата, которые пользователи пишут в приложении, используется Firestore .
Модель данных
Firestore — это база данных NoSQL, и данные в ней разделены на коллекции, документы, поля и подколлекции. Каждое сообщение чата хранится как документ в коллекции guestbook
, которая является коллекцией верхнего уровня.
Добавить сообщения в Firestore
В этом разделе вы добавите функционал, позволяющий пользователям отправлять сообщения в базу данных. Сначала вы добавите поле формы и кнопку отправки, а затем добавите код, связывающий эти элементы с базой данных.
- Создайте новый файл с именем
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
заполнять всё свободное пространство в строке. Чтобы лучше понять, зачем это нужно, см. раздел «Понимание ограничений» .
Теперь, когда у вас есть виджет, позволяющий пользователю вводить текст для добавления в гостевую книгу, вам нужно вывести его на экран.
- Отредактируйте тело
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
— это ссылка на автоматически сгенерированный уникальный идентификатор, который аутентификация присваивает всем вошедшим в систему пользователям.
- Добавьте метод
addMessageToGuestBook
в файлlib/app_state.dart
. Подключение этой возможности к пользовательскому интерфейсу будет выполнено на следующем шаге.
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>
, чтобы сделать состояние приложения доступным для той части дерева, которую вы визуализируете. Это позволяет вам реагировать на ввод сообщения в пользовательском интерфейсе и публиковать его в базе данных. В следующем разделе вы проверите, опубликованы ли добавленные сообщения в базе данных.
Тестовая отправка сообщений
- При необходимости войдите в приложение.
- Введите сообщение, например
Hey there!
, а затем нажмите ОТПРАВИТЬ .
Это действие записывает сообщение в базу данных Firestore. Однако вы не увидите его в самом приложении Flutter, поскольку вам ещё нужно реализовать извлечение данных, что вы сделаете на следующем шаге. Однако на панели управления базой данных консоли Firebase вы увидите добавленное сообщение в коллекцию guestbook
. Отправка дополнительных сообщений добавит новые документы в коллекцию guestbook
. Например, см. следующий фрагмент кода:
7. Прочитать сообщения
Здорово, что гости могут писать сообщения в базу данных, но пока не видят их в приложении. Пора это исправить!
Синхронизировать сообщения
Для отображения сообщений необходимо добавить прослушиватели, срабатывающие при изменении данных, а затем создать элемент пользовательского интерфейса, отображающий новые сообщения. В состояние приложения добавляется код, который отслеживает новые сообщения из приложения.
- Создайте новый файл
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;
}
- В файле
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
- В разделе
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.
- В разделе инициализации
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 .
- В файле
lib/guest_book.dart
добавьте следующий импорт:
import 'guest_book_message.dart';
- В виджете
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();
}
- В
_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
, а затем добавляете коллекцию в конце дочерних элементов Column
, чтобы сгенерировать новый Paragraph
для каждого сообщения в списке сообщений.
- Обновите тело
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 автоматически и мгновенно синхронизирует данные с клиентами, подписанными на базу данных.
Синхронизация тестового сообщения:
- В приложении найдите сообщения, которые вы создали ранее в базе данных.
- Пишите новые сообщения. Они появляются мгновенно.
- Откройте рабочее пространство в нескольких окнах или вкладках. Сообщения синхронизируются в режиме реального времени между окнами и вкладками.
- Необязательно: в меню «База данных» консоли Firebase можно вручную удалить, изменить или добавить новые сообщения. Все изменения отображаются в интерфейсе.
Поздравляем! Вы читаете документы Firestore в своём приложении!
Предварительный просмотр приложения
8. Установите основные правила безопасности.
Изначально вы настраиваете Firestore для использования тестового режима, что означает, что ваша база данных открыта для чтения и записи. Однако тестовый режим следует использовать только на ранних этапах разработки. Рекомендуется настроить правила безопасности для базы данных в процессе разработки приложения. Безопасность — неотъемлемая часть структуры и поведения вашего приложения.
Правила безопасности Firebase позволяют контролировать доступ к документам и коллекциям в вашей базе данных. Гибкий синтаксис правил позволяет создавать правила, которые будут применяться к чему угодно: от всех операций записи во всей базе данных до операций с конкретным документом.
Настройте основные правила безопасности:
- В меню «Разработка» консоли Firebase выберите «База данных» > «Правила» . Вы увидите следующие правила безопасности по умолчанию и предупреждение об их публичности:
- Определите коллекции, в которые приложение записывает данные:
В 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/app_state.dart
добавьте следующие строки в раздел accessorsApplicationState
, чтобы код пользовательского интерфейса мог взаимодействовать с этим состоянием:
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});
}
}
- Обновите метод
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();
});
}
Этот код добавляет запрос по постоянной подписке для определения количества участников и второй запрос, который активен только пока пользователь вошел в систему, для определения, посещает ли пользователь мероприятие.
- Добавьте следующее перечисление в начало файла
lib/app_state.dart
.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Создайте новый файл
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'),
),
],
),
);
}
}
}
Он начинается в неопределённом состоянии, без выбора « Да» или «Нет» . Как только пользователь выбирает, собирается ли он посещать мероприятие, вы показываете этот вариант подсвеченным с помощью заполненной кнопки, а другой вариант исчезает с помощью плоской визуализации.
- Обновите метод
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
.
- В коллекции
attendees
возьмите UID аутентификации, который вы использовали в качестве имени документа, и проверьте, чтоuid
отправителя совпадает с UID создаваемого им документа:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
Это позволяет всем просматривать список участников, поскольку там нет личных данных, но обновлять его может только создатель.
- Добавьте проверку данных, чтобы убедиться, что все ожидаемые поля присутствуют в документе:
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;
}
}
}
- Дополнительно: в приложении нажимайте кнопки, чтобы увидеть результаты на панели управления Firestore в консоли Firebase.
Предварительный просмотр приложения
10. Поздравляем!
Вы использовали Firebase для создания интерактивного веб-приложения, работающего в режиме реального времени!