1. Trước khi bắt đầu
Trong lớp học lập trình này, bạn sẽ tìm hiểu một số kiến thức cơ bản về Firebase để tạo ứng dụng di động Flutter dành cho Android và iOS.
Điều kiện tiên quyết
- Quen thuộc với Flutter
- SDK Flutter
- Trình chỉnh sửa văn bản tuỳ ý
Kiến thức bạn sẽ học được
- Cách tạo ứng dụng trả lời sự kiện và trò chuyện trên sổ lưu bút trên Android, iOS, Web và macOS bằng Flutter.
- Cách xác thực người dùng bằng tính năng Xác thực Firebase và đồng bộ hoá dữ liệu với Firestore.
Bạn cần có
Bất kỳ thiết bị nào sau đây:
- Một thiết bị Android hoặc iOS thực đã kết nối với máy tính của bạn và đặt ở chế độ nhà phát triển.
- Trình mô phỏng iOS (Cần có công cụ Xcode).
- Trình mô phỏng Android (Cần thiết lập trong Android Studio).
Bạn cũng cần có:
- Một trình duyệt mà bạn chọn, chẳng hạn như Google Chrome.
- IDE hoặc trình chỉnh sửa văn bản mà bạn chọn được định cấu hình bằng các trình bổ trợ Dart và Flutter, chẳng hạn như Android Studio hoặc Visual Studio Code.
- Phiên bản
stable
mới nhất của Flutter hoặcbeta
nếu bạn thích sống ngang hàng. - Tài khoản Google để tạo và quản lý dự án Firebase của bạn.
Firebase
CLI đã đăng nhập vào Tài khoản Google của bạn.
2. Nhận mã mẫu
Tải phiên bản ban đầu của dự án xuống từ GitHub:
- Trên dòng lệnh, hãy sao chép kho lưu trữ GitHub trong thư mục
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Thư mục flutter-codelabs
chứa mã nguồn cho một tập hợp lớp học lập trình. Mã dành cho lớp học lập trình này nằm trong thư mục flutter-codelabs/firebase-get-to-know-flutter
. Thư mục này chứa một loạt các ảnh chụp nhanh cho biết dự án của bạn sẽ trông như thế nào ở cuối mỗi bước. Ví dụ: bạn đang ở bước thứ hai.
- Tìm các tệp phù hợp cho bước thứ hai:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
Nếu bạn muốn bỏ qua hoặc xem một bước nào đó sẽ diễn ra như thế nào sau một bước, hãy tìm trong thư mục có tên sau bước mà bạn quan tâm.
Nhập ứng dụng khởi đầu
- Mở hoặc nhập thư mục
flutter-codelabs/firebase-get-to-know-flutter/step_02
trong IDE mà bạn muốn dùng. Thư mục này chứa đoạn mã khởi đầu dành cho lớp học lập trình này, trong đó có một ứng dụng Flutter gặp mặt chưa có chức năng.
Xác định các tệp cần xử lý
Mã trong ứng dụng này được trải rộng trên nhiều thư mục. Việc phân chia chức năng này giúp công việc dễ dàng hơn vì nhóm mã theo chức năng.
- Xác định các tệp sau:
lib/main.dart
: Tệp này chứa điểm truy cập chính và tiện ích ứng dụng.lib/home_page.dart
: Tệp này chứa tiện ích trang chủ.lib/src/widgets.dart
: Tệp này chứa một số tiện ích giúp chuẩn hoá kiểu ứng dụng. Họ soạn màn hình của ứng dụng khởi đầu.lib/src/authentication.dart
: Tệp này chứa cách triển khai một phần của tính năng Xác thực với một tập hợp các tiện ích giúp tạo trải nghiệm người dùng khi đăng nhập cho tính năng xác thực dựa trên email trong Firebase. Các tiện ích này cho quy trình xác thực chưa được sử dụng trong ứng dụng khởi đầu, nhưng sẽ sớm được thêm vào.
Bạn thêm các tệp bổ sung theo yêu cầu để xây dựng phần còn lại của ứng dụng.
Xem lại tệp lib/main.dart
Ứng dụng này tận dụng gói google_fonts
để đặt Roboto làm phông chữ mặc định trong toàn bộ ứng dụng. Bạn có thể khám phá trang fonts.google.com và dùng các phông chữ có ở đó trong nhiều phần của ứng dụng.
Bạn sử dụng các tiện ích trợ giúp từ tệp lib/src/widgets.dart
dưới dạng Header
, Paragraph
và IconAndDetail
. Các tiện ích này loại bỏ mã trùng lặp để bố cục trang không lộn xộn như được mô tả trong HomePage
. Việc này cũng giúp mang lại giao diện nhất quán.
Dưới đây là giao diện của ứng dụng trên Android, iOS, Web và macOS:
3. Tạo và định cấu hình dự án Firebase
Việc hiển thị thông tin sự kiện rất phù hợp cho khách mời của bạn, nhưng sẽ không hữu ích cho tất cả mọi người. Bạn cần thêm một số chức năng động vào ứng dụng. Để thực hiện việc này, bạn cần phải kết nối Firebase với ứng dụng của mình. Để bắt đầu sử dụng Firebase, bạn cần tạo và định cấu hình một dự án Firebase.
Tạo một dự án Firebase
- Đăng nhập vào Firebase.
- Trong bảng điều khiển, hãy nhấp vào Add Project (Thêm dự án) hoặc Create a project (Tạo dự án).
- Trong trường Project name (Tên dự án), hãy nhập Firebase-Flutter-Codelab, sau đó nhấp vào Continue (Tiếp tục).
- Nhấp vào các lựa chọn tạo dự án. Nếu được nhắc, hãy chấp nhận các điều khoản của Firebase, nhưng bỏ qua bước thiết lập Google Analytics vì bạn sẽ không sử dụng Google Analytics cho ứng dụng này.
Để tìm hiểu thêm về các dự án Firebase, hãy xem bài viết Tìm hiểu về các dự án Firebase.
Ứng dụng sử dụng các sản phẩm Firebase có sẵn cho ứng dụng web sau đây:
- Xác thực: Cho phép người dùng đăng nhập vào ứng dụng của bạn.
- Firestore: Lưu dữ liệu có cấu trúc trên đám mây và nhận thông báo tức thì khi dữ liệu thay đổi.
- Quy tắc bảo mật của Firebase: Bảo mật cơ sở dữ liệu của bạn.
Một số sản phẩm trong số này cần cấu hình đặc biệt hoặc bạn cần bật các sản phẩm đó trong bảng điều khiển của Firebase.
Bật tính năng xác thực đăng nhập bằng email
- Trong ngăn Project Overview (Tổng quan về dự án) trên bảng điều khiển của Firebase, hãy mở rộng trình đơn Build (Tạo).
- Nhấp vào Xác thực > Bắt đầu > Phương thức đăng nhập > Email/Mật khẩu > Bật > Lưu.
Bật Firestore
Ứng dụng web sử dụng tính năng Firestore để lưu tin nhắn trò chuyện và nhận tin nhắn trò chuyện mới.
Bật Firestore:
- Trong trình đơn Build (Tạo), hãy nhấp vào Firestore Database > (Cơ sở dữ liệu Khôi phục dữ liệu) > Tạo cơ sở dữ liệu.
- Chọn Bắt đầu ở chế độ thử nghiệm, sau đó đọc tuyên bố từ chối trách nhiệm về các quy tắc bảo mật. Chế độ kiểm thử đảm bảo rằng bạn có thể tự do ghi vào cơ sở dữ liệu trong quá trình phát triển.
- Nhấp vào Tiếp theo, rồi chọn vị trí cho cơ sở dữ liệu của bạn. Bạn có thể dùng giá trị mặc định. Bạn không thể thay đổi vị trí sau này.
- Nhấp vào Bật.
4. Định cấu hình Firebase
Để sử dụng Firebase với Flutter, bạn cần hoàn thành các công việc sau nhằm định cấu hình dự án Flutter để sử dụng đúng các thư viện FlutterFire
:
- Thêm các phần phụ thuộc
FlutterFire
vào dự án. - Đăng ký nền tảng mong muốn trên dự án Firebase.
- Tải tệp cấu hình dành riêng cho nền tảng xuống rồi thêm tệp này vào mã.
Trong thư mục cấp cao nhất của ứng dụng Flutter, có các thư mục con android
, ios
, macos
và web
chứa các tệp cấu hình dành riêng cho nền tảng tương ứng cho iOS và Android.
Định cấu hình phần phụ thuộc
Bạn cần thêm thư viện FlutterFire
cho hai sản phẩm Firebase mà bạn sử dụng trong ứng dụng này: Authentication và Firestore.
- Từ dòng lệnh, hãy thêm các phần phụ thuộc sau:
$ flutter pub add firebase_core
Gói firebase_core
là mã chung cần có cho tất cả các trình bổ trợ Flutter của Firebase.
$ flutter pub add firebase_auth
Gói firebase_auth
cho phép tích hợp với tính năng Xác thực.
$ flutter pub add cloud_firestore
Gói cloud_firestore
cho phép truy cập vào bộ nhớ dữ liệu của Firestore.
$ flutter pub add provider
Gói firebase_ui_auth
cung cấp một tập hợp các tiện ích và tiện ích giúp tăng tốc độ của nhà phát triển thông qua quy trình xác thực.
$ flutter pub add firebase_ui_auth
Bạn đã thêm các gói bắt buộc, nhưng cũng cần định cấu hình dự án iOS, Android, macOS và Trình chạy web để sử dụng Firebase đúng cách. Bạn cũng có thể sử dụng gói provider
để phân tách logic nghiệp vụ khỏi logic hiển thị.
Cài đặt FlutterFire CLI
FlutterFire CLI phụ thuộc vào CLI cơ bản của Firebase.
- Nếu bạn chưa thực hiện thao tác này, hãy cài đặt Firebase CLI trên máy của mình.
- Cài đặt FlutterFire CLI:
$ dart pub global activate flutterfire_cli
Sau khi cài đặt, lệnh flutterfire
sẽ có sẵn trên toàn cầu.
Định cấu hình ứng dụng
CLI trích xuất thông tin từ dự án Firebase của bạn và các ứng dụng dự án được chọn để tạo tất cả cấu hình cho một nền tảng cụ thể.
Trong thư mục gốc của ứng dụng, hãy chạy lệnh configure
:
$ flutterfire configure
Lệnh cấu hình sẽ hướng dẫn bạn thực hiện các quy trình sau:
- Chọn một dự án Firebase dựa trên tệp
.firebaserc
hoặc từ Bảng điều khiển của Firebase. - Xác định các nền tảng cho cấu hình, chẳng hạn như Android, iOS, macOS và web.
- Xác định các ứng dụng Firebase cần trích xuất cấu hình. Theo mặc định, CLI sẽ cố gắng tự động so khớp các ứng dụng Firebase dựa trên cấu hình dự án hiện tại của bạn.
- Tạo một tệp
firebase_options.dart
trong dự án.
Định cấu hình macOS
Flutter trên macOS xây dựng các ứng dụng hoàn toàn dạng hộp cát. Vì ứng dụng này tích hợp với mạng để giao tiếp với máy chủ Firebase nên bạn cần định cấu hình ứng dụng bằng đặc quyền của máy khách mạng.
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>
Để biết thêm thông tin, hãy xem bài viết Hỗ trợ Flutter trên máy tính.
5. Thêm chức năng hồi đáp
Giờ đây, khi đã thêm Firebase vào ứng dụng, bạn có thể tạo nút Trả lời để đăng ký tính năng Xác thực cho mọi người. Đối với ứng dụng gốc Android, ứng dụng gốc iOS và Web, có các gói FirebaseUI Auth
được tạo sẵn, nhưng bạn cần tạo tính năng này cho Flutter.
Dự án mà bạn truy xuất trước đó có một tập hợp tiện ích giúp triển khai giao diện người dùng cho hầu hết quy trình xác thực. Bạn triển khai logic nghiệp vụ để tích hợp tính năng Xác thực với ứng dụng.
Thêm logic kinh doanh với gói Provider
Dùng gói provider
để cung cấp đối tượng trạng thái ứng dụng tập trung trên toàn bộ cây tiện ích Flutter của ứng dụng:
- Tạo một tệp mới có tên là
app_state.dart
với nội dung sau:
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();
});
}
}
Các câu lệnh import
giới thiệu tính năng Lõi và tính năng Xác thực của Firebase, lấy gói provider
giúp cung cấp đối tượng trạng thái ứng dụng trên toàn bộ cây tiện ích và bao gồm các tiện ích xác thực từ gói firebase_ui_auth
.
Đối tượng trạng thái ứng dụng ApplicationState
này có một trách nhiệm chính trong bước này, đó là cảnh báo cây tiện ích rằng đã có nội dung cập nhật đối với trạng thái đã xác thực.
Bạn chỉ sử dụng một nhà cung cấp để thông báo cho ứng dụng về trạng thái đăng nhập của người dùng. Để cho phép người dùng đăng nhập, bạn hãy sử dụng giao diện người dùng do gói firebase_ui_auth
cung cấp. Đây là một cách tuyệt vời để nhanh chóng khởi động các màn hình đăng nhập trong ứng dụng của bạn.
Tích hợp quy trình xác thực
- Sửa đổi các mục nhập ở đầu tệp
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';
- Kết nối trạng thái ứng dụng với quá trình khởi chạy ứng dụng rồi thêm quy trình xác thực vào
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
Việc sửa đổi hàm main()
khiến gói nhà cung cấp chịu trách nhiệm tạo bản sao của đối tượng trạng thái ứng dụng bằng tiện ích ChangeNotifierProvider
. Bạn dùng lớp provider
cụ thể này vì đối tượng trạng thái ứng dụng mở rộng lớp ChangeNotifier
, cho phép gói provider
biết thời điểm hiển thị lại các tiện ích phụ thuộc.
- Hãy cập nhật ứng dụng của bạn để xử lý việc điều hướng đến nhiều màn hình mà FirebaseUI cung cấp cho bạn, bằng cách tạo một cấu hình
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
);
}
}
Mỗi màn hình được liên kết với một loại thao tác khác nhau dựa trên trạng thái mới của quy trình xác thực. Sau hầu hết các thay đổi về trạng thái trong quá trình xác thực, bạn có thể chuyển lại về màn hình ưa thích, cho dù đó là màn hình chính hay màn hình khác, chẳng hạn như hồ sơ.
- Trong phương thức xây dựng của lớp
HomePage
, hãy tích hợp trạng thái ứng dụng với tiện íchAuthFunc
:
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!',
),
],
),
);
}
}
Bạn tạo thực thể cho tiện ích AuthFunc
rồi gói tiện ích này trong một tiện ích Consumer
. Tiện ích Người tiêu dùng là cách thông thường mà bạn có thể dùng gói provider
để xây dựng lại một phần của cây khi trạng thái ứng dụng thay đổi. Tiện ích AuthFunc
là các tiện ích bổ sung mà bạn kiểm thử.
Kiểm thử quy trình xác thực
- Trong ứng dụng, hãy nhấn vào nút RSVP (Hồi đáp) để bắt đầu
SignInScreen
.
- Nhập địa chỉ email. Nếu bạn đã đăng ký, hệ thống sẽ nhắc bạn nhập mật khẩu. Nếu không, hệ thống sẽ nhắc bạn hoàn tất biểu mẫu đăng ký.
- Nhập mật khẩu có ít hơn 6 ký tự để kiểm tra quy trình xử lý lỗi. Nếu đã đăng ký, bạn sẽ nhìn thấy mật khẩu của.
- Nhập mật khẩu không chính xác để kiểm tra quy trình xử lý lỗi.
- Nhập mật khẩu chính xác. Bạn thấy trải nghiệm đăng nhập, cho phép người dùng có thể đăng xuất.
6. Viết tin nhắn cho Firestore
Thật tuyệt khi biết rằng người dùng sẽ truy cập, nhưng bạn cần cung cấp cho khách hàng hoạt động khác để làm trong ứng dụng. Nếu họ có thể để lại tin nhắn trong sổ lưu bút thì sao? Họ có thể chia sẻ lý do khiến họ háo hức muốn đến hoặc muốn gặp được ai.
Để lưu trữ tin nhắn trò chuyện mà người dùng viết trong ứng dụng, hãy sử dụng tính năng Firestore.
Mô hình dữ liệu
Firestore là một cơ sở dữ liệu NoSQL và dữ liệu được lưu trữ trong cơ sở dữ liệu này được chia thành các tập hợp, tài liệu, trường và tập hợp con. Bạn sẽ lưu trữ mỗi tin nhắn của cuộc trò chuyện dưới dạng tài liệu trong bộ sưu tập guestbook
, đây là một tập hợp cấp cao nhất.
Thêm thông báo vào Firestore
Trong phần này, bạn thêm chức năng cho phép người dùng ghi thông báo vào cơ sở dữ liệu. Trước tiên, bạn thêm một trường biểu mẫu và nút gửi, sau đó thêm mã kết nối các phần tử này với cơ sở dữ liệu.
- Tạo một tệp mới có tên là
guest_book.dart
, thêm tiện ích có trạng tháiGuestBook
để tạo các thành phần trên giao diện người dùng của trường tin nhắn và nút gửi:
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'),
],
),
),
],
),
),
);
}
}
Có một vài địa điểm yêu thích ở đây. Đầu tiên, bạn tạo thực thể cho một biểu mẫu để có thể xác thực rằng thông báo đó thực sự có chứa nội dung và cho người dùng thấy thông báo lỗi nếu không có thông báo nào. Để xác thực một biểu mẫu, bạn hãy truy cập vào trạng thái biểu mẫu phía sau biểu mẫu bằng GlobalKey
. Để biết thêm thông tin về Khoá và cách sử dụng, hãy xem bài viết Trường hợp sử dụng khoá.
Ngoài ra, hãy lưu ý cách bố trí các tiện ích: bạn có Row
với TextFormField
và StyledButton
chứa Row
. Ngoài ra, xin lưu ý rằng TextFormField
được gói trong một tiện ích Expanded
, buộc TextFormField
phải lấp đầy mọi không gian thừa trong hàng. Để hiểu rõ hơn lý do cần thực hiện điều này, hãy xem phần Tìm hiểu các hạn chế.
Giờ đây, khi đã có tiện ích cho phép người dùng nhập nội dung nào đó để thêm vào Sổ khách, bạn cần đưa nội dung đó lên màn hình.
- Chỉnh sửa nội dung của
HomePage
để thêm hai dòng sau vào cuối phần tử con củaListView
:
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)),
Mặc dù điều này là đủ để hiển thị tiện ích nhưng nó vẫn chưa đủ để làm bất kỳ điều gì hữu ích. Bạn sẽ sớm cập nhật mã này để mã này hoạt động.
Bản xem trước ứng dụng
Khi người dùng nhấp vào nút GỬI, thao tác này sẽ kích hoạt đoạn mã sau đây. Tệp này sẽ thêm nội dung của trường nhập thông báo vào tập hợp guestbook
của cơ sở dữ liệu. Cụ thể, phương thức addMessageToGuestBook
sẽ thêm nội dung thông báo vào một tài liệu mới có mã nhận dạng được tạo tự động trong bộ sưu tập guestbook
.
Xin lưu ý rằng FirebaseAuth.instance.currentUser.uid
là tham chiếu đến mã nhận dạng duy nhất được tạo tự động mà tính năng Xác thực cung cấp cho tất cả người dùng đã đăng nhập.
- Trong tệp
lib/app_state.dart
, hãy thêm phương thứcaddMessageToGuestBook
. Bạn sẽ kết nối chức năng này với giao diện người dùng trong bước tiếp theo.
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.
}
Kết nối giao diện người dùng và cơ sở dữ liệu
Bạn có một giao diện người dùng, trong đó người dùng có thể nhập văn bản họ muốn thêm vào Sổ dành cho khách và bạn có mã để thêm mục nhập vào Firestore. Giờ đây, bạn chỉ cần kết nối cả hai.
- Trong tệp
lib/home_page.dart
, hãy thực hiện thay đổi sau đây cho tiện íchHomePage
:
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.
],
),
);
}
}
Bạn đã thay thế hai dòng mà bạn thêm ở đầu bước này bằng quy trình triển khai đầy đủ. Bạn một lần nữa sử dụng Consumer<ApplicationState>
để cung cấp trạng thái ứng dụng cho phần cây mà bạn kết xuất. Việc này cho phép bạn phản ứng với người dùng nhập thông báo vào giao diện người dùng và xuất bản thông báo đó trong cơ sở dữ liệu. Trong phần tiếp theo, bạn sẽ kiểm tra xem các thông báo đã thêm có được xuất bản trong cơ sở dữ liệu hay không.
Thử gửi tin nhắn
- Nếu cần, hãy đăng nhập vào ứng dụng.
- Nhập một tin nhắn, chẳng hạn như
Hey there!
, sau đó nhấp vào GỬI.
Thao tác này sẽ ghi thông báo vào cơ sở dữ liệu Firestore của bạn. Tuy nhiên, bạn không thấy thông báo này trong ứng dụng Flutter thực tế của mình vì bạn vẫn cần triển khai quá trình truy xuất dữ liệu trong bước tiếp theo. Tuy nhiên, trong trang tổng quan Cơ sở dữ liệu của bảng điều khiển Firebase, bạn có thể thấy thông báo bạn đã thêm trong tập hợp guestbook
. Nếu gửi nhiều thư hơn, bạn sẽ thêm nhiều tài liệu hơn vào bộ sưu tập guestbook
của mình. Ví dụ: hãy xem đoạn mã sau:
7. Đọc tin nhắn
Thật tuyệt vời khi khách có thể viết tin nhắn vào cơ sở dữ liệu nhưng họ chưa thể xem chúng trong ứng dụng. Đã đến lúc khắc phục vấn đề đó!
Đồng bộ hoá thư
Để hiện thông báo, bạn cần thêm trình nghe kích hoạt khi dữ liệu thay đổi, sau đó tạo một thành phần trên giao diện người dùng giúp hiển thị thông báo mới. Bạn thêm mã vào trạng thái ứng dụng để theo dõi các tin nhắn mới được thêm vào từ ứng dụng.
- Tạo một tệp mới
guest_book_message.dart
, thêm lớp sau để hiển thị khung hiển thị có cấu trúc của dữ liệu mà bạn lưu trữ trong Firestore.
lib/guest_book_message.dart
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- Trong tệp
lib/app_state.dart
, hãy thêm các lệnh nhập sau:
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
- Trong phần
ApplicationState
, nơi bạn xác định trạng thái và phương thức getter, hãy thêm các dòng sau:
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.
- Trong phần khởi động của
ApplicationState
, hãy thêm các dòng sau để đăng ký truy vấn qua tập hợp tài liệu khi người dùng đăng nhập và huỷ đăng ký khi họ đăng xuất:
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();
});
}
Phần này rất quan trọng vì đó là nơi bạn tạo truy vấn qua tập hợp guestbook
, đồng thời xử lý việc đăng ký và huỷ đăng ký bộ sưu tập này. Bạn theo dõi luồng này, nơi bạn tạo lại bộ nhớ đệm cục bộ của thư trong bộ sưu tập guestbook
, đồng thời lưu trữ tham chiếu đến gói thuê bao này để có thể huỷ đăng ký sau này. Có rất nhiều vấn đề đang xảy ra ở đây, vì vậy bạn nên khám phá trong trình gỡ lỗi để kiểm tra điều gì xảy ra nhằm có được mô hình tư duy rõ ràng hơn. Để biết thêm thông tin, hãy xem bài viết Nhận thông tin cập nhật theo thời gian thực qua Firestore.
- Trong tệp
lib/guest_book.dart
, hãy thêm dữ liệu nhập sau:
import 'guest_book_message.dart';
- Trong tiện ích
GuestBook
, hãy thêm một danh sách thông báo vào cấu hình để kết nối trạng thái thay đổi này với giao diện người dùng:
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();
}
- Trong
_GuestBookState
, hãy sửa đổi phương thứcbuild
như sau để hiển thị cấu hình này:
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.
);
}
}
Bạn gói nội dung trước đó của phương thức build()
bằng tiện ích Column
, sau đó thêm bộ sưu tập cho ở đuôi phần tử con của Column
để tạo Paragraph
mới cho từng thông báo trong danh sách thông báo.
- Cập nhật phần nội dung của
HomePage
để tạo chính xácGuestBook
bằng tham sốmessages
mới:
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
),
],
],
),
),
Kiểm thử quá trình đồng bộ hoá tin nhắn
Firestore tự động đồng bộ hoá dữ liệu ngay lập tức với các máy khách đã đăng ký cơ sở dữ liệu.
Kiểm thử quá trình đồng bộ hoá tin nhắn:
- Trong ứng dụng, hãy tìm các thông báo mà bạn đã tạo trước đó trong cơ sở dữ liệu.
- Soạn tin nhắn mới. Chúng sẽ xuất hiện ngay lập tức.
- Mở không gian làm việc của bạn trong nhiều cửa sổ hoặc thẻ. Thư sẽ đồng bộ hoá theo thời gian thực giữa các cửa sổ và thẻ.
- Không bắt buộc: Trong trình đơn Cơ sở dữ liệu của bảng điều khiển Firebase, hãy xoá, sửa đổi hoặc thêm thông báo mới theo cách thủ công. Tất cả các thay đổi đều sẽ xuất hiện trong giao diện người dùng.
Xin chúc mừng! Bạn đã đọc tài liệu trên Firestore trong ứng dụng của mình!
Bản xem trước ứng dụng
8. Thiết lập các quy tắc bảo mật cơ bản
Ban đầu, bạn thiết lập Firestore để sử dụng chế độ kiểm thử, tức là cơ sở dữ liệu của bạn có thể đọc và ghi. Tuy nhiên, bạn chỉ nên sử dụng chế độ thử nghiệm trong giai đoạn đầu của quá trình phát triển. Phương pháp hay nhất là bạn nên thiết lập các quy tắc bảo mật cho cơ sở dữ liệu khi phát triển ứng dụng. Bảo mật là yếu tố không thể thiếu trong cấu trúc và hành vi của ứng dụng.
Quy tắc bảo mật của Firebase cho phép bạn kiểm soát quyền truy cập vào các tài liệu và tập hợp trong cơ sở dữ liệu của mình. Cú pháp quy tắc linh hoạt cho phép bạn tạo các quy tắc phù hợp với bất kỳ điều gì, từ mọi lượt ghi vào toàn bộ cơ sở dữ liệu cho đến các thao tác trên một tài liệu cụ thể.
Thiết lập các quy tắc bảo mật cơ bản:
- Trong trình đơn Phát triển của bảng điều khiển Firebase, hãy nhấp vào Cơ sở dữ liệu > Quy tắc. Bạn sẽ thấy các quy tắc bảo mật mặc định sau đây và cảnh báo về các quy tắc đang được công khai:
- Xác định các tập hợp mà ứng dụng ghi dữ liệu vào:
Trong match /databases/{database}/documents
, hãy xác định tập hợp mà bạn muốn bảo mật:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
// You'll add rules here in the next step.
}
}
Do bạn đã sử dụng UID Xác thực làm một trường trong mỗi tài liệu sổ lưu bút, bạn có thể nhận UID Xác thực và xác minh rằng bất kỳ ai cố gắng ghi vào tài liệu đều có UID Xác thực phù hợp.
- Thêm quy tắc đọc và ghi vào bộ quy tắc:
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;
}
}
}
Giờ đây, chỉ người dùng đã đăng nhập mới có thể đọc thư trong sổ lưu niệm, nhưng chỉ tác giả của thư mới có thể chỉnh sửa thư.
- Thêm quy tắc xác thực dữ liệu để đảm bảo rằng tất cả các trường dự kiến đều có trong tài liệu:
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. Bước tặng thêm: Thực hành những gì bạn đã học
Ghi lại trạng thái hồi đáp của người tham dự
Hiện tại, ứng dụng của bạn chỉ cho phép mọi người trò chuyện khi họ quan tâm đến sự kiện. Ngoài ra, cách duy nhất để bạn biết liệu ai đó có đến hay không là khi họ nói như vậy trong cuộc trò chuyện.
Ở bước này, bạn sẽ sắp xếp thông tin và cho mọi người biết số lượng người sẽ tham gia. Bạn thêm một số chức năng vào trạng thái ứng dụng. Thứ nhất là cho phép người dùng đã đăng nhập chỉ định việc họ có tham gia hay không. Thứ hai là để đếm số người sẽ tham gia.
- Trong tệp
lib/app_state.dart
, hãy thêm các dòng sau vào phần trình truy cập củaApplicationState
để mã giao diện người dùng có thể tương tác với trạng thái này:
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});
}
}
- Cập nhật phương thức
init()
củaApplicationState
như sau:
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();
});
}
Mã này thêm một truy vấn luôn được đăng ký để xác định số lượng người tham dự và một truy vấn thứ hai chỉ hoạt động trong khi người dùng đăng nhập để xác định xem người dùng có đang tham dự hay không.
- Thêm giá trị liệt kê sau đây vào đầu tệp
lib/app_state.dart
.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Tạo một tệp mới
yes_no_selection.dart
, xác định một tiện ích mới hoạt động như nút chọn:
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'),
),
],
),
);
}
}
}
Sự kiện này bắt đầu ở trạng thái không xác định khi không chọn Có hay Không. Sau khi người dùng chọn liệu họ có tham gia hay không, bạn sẽ làm nổi bật tuỳ chọn đó bằng một nút được tô màu nền và tuỳ chọn khác thu lại với kết xuất phẳng.
- Cập nhật phương thức
build()
củaHomePage
để tận dụngYesNoSelection
, cho phép người dùng đã đăng nhập chỉ định việc họ có tham dự hay không và hiển thị số người tham dự sự kiện:
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,
),
],
],
),
),
Thêm quy tắc
Bạn đã thiết lập một số quy tắc, do đó, dữ liệu mà bạn thêm bằng các nút sẽ bị từ chối. Bạn cần cập nhật các quy tắc để cho phép bổ sung vào tập hợp attendees
.
- Trong tập hợp
attendees
, hãy lấy UID xác thực mà bạn đã dùng làm tên tài liệu và xác minh rằnguid
của người gửi giống với tài liệu mà họ đang viết:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
Việc này cho phép mọi người đọc danh sách người tham dự vì không có dữ liệu riêng tư ở đó mà chỉ người tạo mới có thể cập nhật danh sách đó.
- Thêm quy trình xác thực dữ liệu để đảm bảo rằng tất cả các trường dự kiến đều có trong tài liệu:
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;
}
}
}
- Không bắt buộc: Trong ứng dụng, hãy nhấp vào các nút để xem kết quả trên trang tổng quan của Firestore trong bảng điều khiển của Firebase.
Bản xem trước ứng dụng
10. Xin chúc mừng!
Bạn đã sử dụng Firebase để tạo ứng dụng web theo thời gian thực, có tính tương tác!