1. Прежде чем начать
В этой лаборатории кода вы изучите некоторые основы Firebase для создания мобильных приложений Flutter для Android и iOS.
Предварительные условия
- Знакомство с Флаттером
- Флаттер SDK
- Текстовый редактор на ваш выбор
Что вы узнаете
- Как создать приложение для ответа на приглашение на мероприятие и чата с гостевой книгой на Android, iOS, в Интернете и macOS с помощью Flutter.
- Как аутентифицировать пользователей с помощью Firebase Authentication и синхронизировать данные с Firestore.
Что вам понадобится
Любое из следующих устройств:
- Физическое устройство 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:
- Из командной строки клонируйте репозиторий GitHub в каталог
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Каталог flutter-codelabs
содержит код для коллекции 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.
Найдите файлы, над которыми нужно работать
Код в этом приложении разбросан по нескольким каталогам. Такое разделение функциональности упрощает работу, поскольку группирует код по функциональности.
- Найдите следующие файлы:
-
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 .
- В консоли нажмите «Добавить проект» или «Создать проект» .
- В поле «Имя проекта» введите Firebase-Flutter-Codelab и нажмите « Продолжить» .
- Просмотрите параметры создания проекта. При появлении запроса примите условия Firebase, но пропустите настройку Google Analytics, поскольку вы не будете использовать его для этого приложения.
Дополнительные сведения о проектах Firebase см. в разделе Общие сведения о проектах Firebase .
В приложении используются следующие продукты Firebase, доступные для веб-приложений:
- Аутентификация: позволяет пользователям входить в ваше приложение.
- Firestore: сохраняет структурированные данные в облаке и получает мгновенные уведомления при изменении данных.
- Правила безопасности Firebase: защищает вашу базу данных.
Некоторые из этих продуктов требуют специальной настройки или их необходимо включить в консоли Firebase.
Включить аутентификацию при входе по электронной почте
- На панели «Обзор проекта» консоли Firebase разверните меню «Сборка» .
- Нажмите «Аутентификация» > «Начало работы» > «Метод входа» > «Электронная почта/пароль» > «Включить» > «Сохранить» .
Включить Firestore
Веб-приложение использует Firestore для сохранения сообщений чата и получения новых сообщений чата.
Включить Firestore:
- В меню «Создать» выберите «База данных Firestore» > «Создать базу данных» .
- Выберите «Начать в тестовом режиме» , а затем прочтите заявление об отказе от ответственности о правилах безопасности. Тестовый режим гарантирует, что вы можете свободно писать в базу данных во время разработки.
- Нажмите «Далее» , а затем выберите место для вашей базы данных. Вы можете использовать значение по умолчанию. Позже вы не сможете изменить местоположение.
- Нажмите Включить .
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.
Интерфейс командной строки FlutterFire зависит от базового интерфейса командной строки Firebase.
- Если вы еще этого не сделали, установите Firebase CLI на свой компьютер.
- Установите интерфейс командной строки FlutterFire:
$ dart pub global activate flutterfire_cli
После установки команда flutterfire
доступна по всему миру.
Настройте свои приложения
Интерфейс командной строки извлекает информацию из вашего проекта 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 , которая будет регистрировать людей с помощью аутентификации . Для 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
:
библиотека/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
:
библиотека/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
:
библиотека/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
— это дополнительные виджеты, которые вы тестируете.
Проверьте поток аутентификации
- В приложении нажмите кнопку «Ответить» , чтобы запустить
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
— это ссылка на автоматически сгенерированный уникальный идентификатор, который аутентификация предоставляет всем вошедшим в систему пользователям.
- В файле
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>
, чтобы сделать состояние приложения доступным для той части дерева, которую вы визуализируете. Это позволяет вам реагировать на кого-то, кто вводит сообщение в пользовательский интерфейс, и публиковать его в базе данных. В следующем разделе вы проверите, публикуются ли добавленные сообщения в базе данных.
Тестовая отправка сообщений
- При необходимости войдите в приложение.
- Введите сообщение, например «
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
, а затем добавляете коллекцию for в хвост дочерних элементов 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. Бонусный шаг: попрактикуйтесь в том, что вы узнали
Запишите статус ответа участника
Сейчас ваше приложение позволяет людям общаться в чате только тогда, когда они заинтересованы в событии. Кроме того, единственный способ узнать, придет ли кто-то, — это сказать об этом в чате.
На этом этапе вы организуетесь и сообщаете людям, сколько людей придет. Вы добавляете пару возможностей в состояние приложения. Во-первых, это возможность вошедшему в систему пользователю указать, посещают ли он мероприятие. Второй — счетчик количества присутствующих.
- В файле
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});
}
}
- Обновите метод
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
отправителя совпадает с идентификатором документа, который он пишет:
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 для создания интерактивного веб-приложения, работающего в реальном времени!