Tìm hiểu về Firebase dành cho Flutter

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 cho Android và iOS.

Điều kiện tiên quyết

Kiến thức bạn sẽ học được

  • Cách tạo ứng dụng trò chuyện sổ lưu bút và phản hồi tham dự sự kiện 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.

Màn hình chính của ứng dụng trên Android

Màn hình chính của ứng dụng trên iOS

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 được kết nối với máy tính và được đặt ở chế độ nhà phát triển.
  • Trình mô phỏng iOS (Yêu cầu các công cụ Xcode).
  • Trình mô phỏng Android (Yêu cầu thiết lập trong Android Studio).

Bạn cũng cần có những thông tin sau:

  • Một trình duyệt mà bạn chọn, chẳng hạn như Google Chrome.
  • Một 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ặc beta nếu bạn thích trải nghiệm những tính năng mới nhất.
  • Một Tài khoản Google để tạo và quản lý dự án Firebase.
  • FirebaseCLI đã đă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:

  1. Từ 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ã cho một tập hợp các lớp học lập trình. Đoạn 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 ảnh chụp nhanh cho thấy giao diện của dự án sau khi hoàn tất mỗi bước. Ví dụ: bạn đang ở bước thứ hai.

  1. Tìm các tệp trùng khớ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ẽ trông như thế nào, hãy xem trong thư mục có tên theo 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. Thư mục này chứa đoạn mã khởi đầu cho lớp học lập trình, bao gồm một ứng dụng Flutter meetup chưa hoạt động.

Tìm những 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 trở nên dễ dàng hơn vì nó nhóm mã theo chức năng.

  • Tìm 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ố ít các tiện ích giúp chuẩn hoá kiểu của ứng dụng. Các tiện ích này tạo nên màn hình của ứng dụng khởi đầu.
    • lib/src/authentication.dart: Tệp này chứa một phần của quá trình triển khai Xác thực bằng một nhóm các tiện ích để tạo trải nghiệm đăng nhập cho người dùng đối với tính năng xác thực dựa trên email của Firebase. Các tiện ích này cho quy trình xác thực chưa được dùng trong ứng dụng khởi động, nhưng bạn sẽ sớm thêm chúng.

Bạn thêm các tệp bổ sung khi cần để tạo phần còn lại của ứng dụng.

Xem xét 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á fonts.google.com và sử dụng các phông chữ mà bạn tìm thấy ở đó 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, ParagraphIconAndDetail. Các tiện ích này loại bỏ mã trùng lặp để giảm sự lộn xộn trong bố cục trang được mô tả trong HomePage. Điều này cũng giúp tạo ra giao diện nhất quán.

Ứng dụng của bạn sẽ có giao diện như sau trên Android, iOS, Web và macOS:

Màn hình chính của ứng dụng trên Android

Màn hình chính của ứng dụng trên iOS

Màn hình chính của ứng dụng trên web

Màn hình chính của ứng dụng trên macOS

3. Tạo và thiết lập dự án Firebase

Việc hiển thị thông tin sự kiện rất hữu ích cho khách của bạn, nhưng không hữu ích cho bất kỳ ai khác. Bạn cần thêm một số chức năng động vào ứng dụng. Để làm như vậy, bạn cần 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à thiết lập một dự án Firebase.

Tạo một dự án Firebase

  1. Đăng nhập vào bảng điều khiển của Firebase bằng Tài khoản Google của bạn.
  2. Nhấp vào nút này để tạo một dự án mới, rồi nhập tên dự án (ví dụ: Firebase-Flutter-Codelab).
  3. Nhấp vào Tiếp tục.
  4. Nếu được nhắc, hãy xem xét và chấp nhận các điều khoản của Firebase, rồi nhấp vào Tiếp tục.
  5. (Không bắt buộc) Bật tính năng hỗ trợ của AI trong bảng điều khiển của Firebase (còn gọi là "Gemini trong Firebase").
  6. Đối với lớp học lập trình này, bạn không cần Google Analytics, vì vậy hãy tắt lựa chọn Google Analytics.
  7. Nhấp vào Tạo dự án, đợi dự án được cấp phép rồi nhấp vào Tiếp tục.

Để 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.

Thiết lập các sản phẩm của Firebase

Ứng dụng này sử dụng các sản phẩm Firebase sau đây (có sẵn cho ứng dụng web):

  • 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ó cấu hình đặc biệt hoặc bạn cần bật chúng 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

  1. Trong ngăn Tổng quan về dự án của bảng điều khiển Firebase, hãy mở rộng trình đơn Tạo.
  2. 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.

58e3e3e23c2f16a4.png

Thiết lập Firestore

Ứng dụng web này sử dụng Firestore để lưu tin nhắn trò chuyện và nhận tin nhắn trò chuyện mới.

Sau đây là cách thiết lập Firestore trong dự án Firebase:

  1. Trong bảng điều khiển bên trái của bảng điều khiển Firebase, hãy mở rộng mục Tạo rồi chọn Cơ sở dữ liệu Firestore.
  2. Nhấp vào Tạo cơ sở dữ liệu.
  3. Để nguyên Mã cơ sở dữ liệu được đặt thành (default).
  4. Chọn một vị trí cho cơ sở dữ liệu của bạn, rồi nhấp vào Tiếp theo.
    Đối với một ứng dụng thực tế, bạn nên chọn một vị trí gần với người dùng của mình.
  5. Nhấp vào Bắt đầu ở chế độ thử nghiệm. Đọc tuyên bố từ chối trách nhiệm về các quy tắc bảo mật.
    Sau này trong lớp học lập trình này, bạn sẽ thêm Quy tắc bảo mật để bảo mật dữ liệu của mình. Không phân phối hoặc công khai một ứng dụng mà không thêm Quy tắc bảo mật cho cơ sở dữ liệu của bạn.
  6. Nhấp vào Tạo.

4. Định cấu hình Firebase

Để sử dụng Firebase với Flutter, bạn cần hoàn tất các thao tác sau để định cấu hình dự án Flutter nhằm sử dụng đúng các thư viện FlutterFire:

  1. Thêm các phần phụ thuộc FlutterFire vào dự án của bạn.
  2. Đăng ký nền tảng mong muốn trên dự án Firebase.
  3. 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 đó 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, macosweb. Các thư mục này lần lượt chứa các tệp cấu hình dành riêng cho từng nền tảng cho iOS và Android.

Định cấu hình các phần phụ thuộc

Bạn cần thêm các thư viện FlutterFire cho 2 sản phẩm Firebase mà bạn sử dụng trong ứng dụng này: Xác thực và Firestore.

  • Trên 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 bắt buộc đối với tất cả các trình bổ trợ Firebase Flutter.

$ flutter pub add firebase_auth

Gói firebase_auth cho phép tích hợp với 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 Firestore.

$ flutter pub add provider

Gói firebase_ui_auth cung cấp một bộ tiện ích và tiện ích giúp tăng tốc độ phát triển bằng các 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 bạn cũng cần định cấu hình các dự án trình chạy iOS, Android, macOS và Web để sử dụng Firebase một cách thích hợp. Bạn cũng sử dụng gói provider cho phép tách logic nghiệp vụ khỏi logic hiển thị.

Cài đặt FlutterFire CLI

FlutterFire CLI phụ thuộc vào Firebase CLI cơ bản.

  1. Nếu chưa, hãy cài đặt Firebase CLI trên máy của bạn.
  2. Cài đặt FlutterFire CLI:
$ dart pub global activate flutterfire_cli

Sau khi cài đặt, lệnh flutterfire sẽ có hiệu lực trên toàn cầu.

Định cấu hình ứng dụng

CLI sẽ trích xuất thông tin từ dự án Firebase và các ứng dụng dự án đã 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 định cấu hình sẽ hướng dẫn bạn thực hiện các quy trình sau:

  1. 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.
  2. Xác định các nền tảng cho cấu hình, chẳng hạn như Android, iOS, macOS và web.
  3. Xác định những ứng dụng Firebase mà bạn muố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.
  4. Tạo tệp firebase_options.dart trong dự án của bạn.

Định cấu hình macOS

Flutter trên macOS tạo các ứng dụng hoàn toàn được cách ly. Vì ứng dụng này tích hợp với mạng để giao tiếp với các máy chủ Firebase, nên bạn cần định cấu hình ứng dụng của mình bằng các đặc quyền của ứng dụng 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ợ máy tính cho Flutter.

5. Thêm chức năng trả lời

Giờ đây, sau khi thêm Firebase vào ứng dụng, bạn có thể tạo nút RSVP (Trả lời tham dự) để đăng ký người dùng bằng tính năng Xác thực. Đối với Android gốc, iOS gốc và Web, có các gói FirebaseUI Auth được tạo sẵn, nhưng bạn cần tạo chức năng này cho Flutter.

Dự án mà bạn đã truy xuất trước đó bao gồm một nhóm các tiện ích 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 quy trình Xác thực với ứng dụng.

Thêm logic nghiệp vụ bằng gói Provider

Sử dụng gói provider để cung cấp một đối tượng trạng thái ứng dụng tập trung trong toàn bộ cây tiện ích Flutter của ứng dụng:

  1. Tạo một tệp mới có tên 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 Firebase Core và Auth, kéo gói provider giúp đối tượng trạng thái ứng dụng có sẵn trong 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 cho bước này, đó là cảnh báo cây tiện ích rằng đã có một bản cập nhật cho trạng thái đã xác thực.

Bạn chỉ sử dụng một nhà cung cấp để truyền đạt trạng thái đăng nhập của người dùng cho ứng dụng. Để cho phép người dùng đăng nhập, bạn 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 màn hình đăng nhập trong các ứng dụng của bạn.

Tích hợp quy trình xác thực

  1. 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';
  1. Kết nối trạng thái ứng dụng với quá trình khởi chạy ứng dụng, sau đó 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 về việc khởi tạo đối tượng trạng thái ứng dụng bằng tiện ích ChangeNotifierProvider. Bạn sử 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.

  1. Cập nhật ứng dụng để xử lý hoạt động điều hướng đến các màn hình mà FirebaseUI cung cấp cho bạn bằng cách tạo 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ó một loại thao tác riêng liên kết với màn hình đó 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 hướng trở lại màn hình ưu tiên, cho dù đó là màn hình chính hay một màn hình khác, chẳng hạn như hồ sơ.

  1. Trong phương thức tạo của lớp HomePage, hãy tích hợp trạng thái ứng dụng với tiện ích 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!',
          ),
        ],
      ),
    );
  }
}

Bạn tạo thực thể cho tiện ích AuthFunc và gói tiện ích đó trong tiện ích Consumer. Tiện ích Consumer là cách thông thường mà gói provider có thể được dùng để tạo 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à tiện ích bổ sung mà bạn kiểm thử.

Kiểm thử quy trình xác thực

cdf2d25e436bd48d.png

  1. Trong ứng dụng, hãy nhấn vào nút RSVP để bắt đầu SignInScreen.

2a2cd6d69d172369.png

  1. 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ý.

e5e65065dba36b54.png

  1. 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ẽ thấy mật khẩu cho thay vì.
  2. Nhập mật khẩu không chính xác để kiểm tra quy trình xử lý lỗi.
  3. Nhập mật khẩu chính xác. Bạn sẽ thấy trải nghiệm đăng nhập, cho phép người dùng đăng xuất.

4ed811a25b0cf816.png

6. Ghi thông báo vào Firestore

Thật tuyệt khi biết rằng người dùng đang đến, nhưng bạn cần cung cấp cho khách những việc khác để làm trong ứng dụng. Chẳng hạn như họ có thể để lại tin nhắn trong sổ lưu bút. Họ có thể chia sẻ lý do khiến họ hào hứng tham gia hoặc người mà họ hy vọng sẽ gặp.

Để lưu trữ các tin nhắn trò chuyện mà người dùng viết trong ứng dụng, bạn sẽ sử dụ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 bộ sưu tập, tài liệu, trường và bộ sưu tập con. Bạn lưu trữ từng tin nhắn của cuộc trò chuyện dưới dạng một tài liệu trong một tập hợp guestbook. Đây là một tập hợp cấp cao nhất.

7c20dc8424bb1d84.png

Thêm thông báo vào Firestore

Trong phần này, bạn sẽ thêm chức năng để người dùng viết 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.

  1. Tạo một tệp mới có tên guest_book.dart, thêm một tiện ích có trạng thái GuestBook để tạo các phần tử giao diện người dùng của một trường tin nhắn và một 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 điểm đáng chú ý ở đây. Trước tiên, bạn sẽ tạo 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ó nội dung nào. Để xác thực một biểu mẫu, bạn truy cập vào trạng thái biểu mẫu đằng 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 Thời điểm 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 TextFormFieldStyledButton, trong đó có Row. Cũng lưu ý rằng TextFormField được bao bọc trong một tiện ích Expanded, tiện ích này buộc TextFormField lấp đầy mọi khoảng trống thừa trong hàng. Để hiểu rõ hơn lý do cần có điều này, hãy xem phần Tìm hiểu các điều kiện ràng buộc.

Giờ đây, bạn đã có một tiện ích cho phép người dùng nhập một số văn bản để thêm vào Sổ khách. Bạn cần đưa tiện ích này lên màn hình.

  1. Chỉnh sửa nội dung của HomePage để thêm 2 dòng sau vào cuối các phần tử con của 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)),

Mặc dù đủ để hiển thị tiện ích, nhưng điều này không đủ để làm bất cứ điều gì hữu ích. Bạn sẽ cập nhật mã này trong thời gian ngắn để mã hoạt động được.

Bản xem trước ứng dụng

Màn hình chính của ứng dụng trên Android có tích hợp tính năng trò chuyện

Màn hình chính của ứng dụng trên iOS có tích hợp tính năng trò chuyện

Màn hình chính của ứng dụng trên web có tích hợp tính năng trò chuyện

Màn hình chính của ứng dụng trên macOS có tích hợp tính năng trò chuyện

Khi người dùng nhấp vào GỬI, thao tác này sẽ kích hoạt đoạn mã sau. Thao tác 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 tin nhắn vào một tài liệu mới có mã nhận dạng được tạo tự động trong tập hợp guestbook.

Xin lưu ý rằng FirebaseAuth.instance.currentUser.uid là một giá trị tham chiếu đến mã nhận dạng duy nhất được tạo tự động mà Authentication 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ức addMessageToGuestBook. Bạn sẽ kết nối chức năng này với giao diện người dùng ở 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 nơi người dùng có thể nhập văn bản mà họ muốn thêm vào Sổ lưu bút 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 hai thiết bị này.

  • Trong tệp lib/home_page.dart, hãy thực hiện thay đổi sau đối với tiện ích 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.
        ],
      ),
    );
  }
}

Bạn đã thay thế 2 dòng mà bạn thêm vào đầu bước này bằng cách triển khai đầy đủ. Bạn sẽ dùng lại Consumer<ApplicationState> để cung cấp trạng thái ứng dụng cho phần cây mà bạn kết xuất. Điều này cho phép bạn phản hồi người nhập tin nhắn vào giao diện người dùng và xuất bản tin nhắn đó trong cơ sở dữ liệu. Trong phần tiếp theo, bạn sẽ kiểm thử 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

  1. Đăng nhập vào ứng dụng nếu cần.
  2. Nhập một tin nhắn, chẳng hạn như Hey there!, rồi 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 sẽ không thấy thông báo trong ứng dụng Flutter thực tế vì bạn vẫn cần triển khai việc truy xuất dữ liệu. Bạn sẽ thực hiện việc này ở 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 mà bạn đã thêm trong tập hợp guestbook. Nếu gửi thêm tin nhắn, bạn sẽ thêm nhiều tài liệu vào bộ sưu tập guestbook. Ví dụ: hãy xem đoạn mã sau:

713870af0b3b63c.png

7. Đọc tin nhắn

Thật tuyệt khi khách có thể viết tin nhắn vào cơ sở dữ liệu, nhưng họ chưa thể xem tin nhắn trong ứng dụng. Đã đến lúc khắc phục vấn đề đó!

Đồng bộ hoá thư

Để hiển thị thông báo, bạn cần thêm các trình nghe kích hoạt khi dữ liệu thay đổi, sau đó tạo một phần tử trên giao diện người dùng 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 thông báo mới được thêm vào ứng dụng.

  1. Tạo một tệp mới guest_book_message.dart, thêm lớp sau để hiển thị một khung hiển thị có cấu trúc về 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;
}
  1. Trong tệp lib/app_state.dart, hãy thêm các mục 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
  1. 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.
  1. Trong phần khởi tạo của ApplicationState, hãy thêm các dòng sau để đăng ký một truy vấn trên bộ sưu tậ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ì đây là nơi bạn tạo một truy vấn trên tập hợp guestbook, đồng thời xử lý việc đăng ký và huỷ đăng ký tập hợp này. Bạn sẽ nghe luồng dữ liệu, nơi bạn tạo lại bộ nhớ đệm cục bộ của các thông báo trong tập hợp guestbook, đồng thời lưu trữ thông tin tham chiếu đến gói thuê bao này để có thể huỷ đăng ký sau này. Có rất nhiều việc đang diễn ra ở đây, vì vậy, bạn nên khám phá nó trong một trình gỡ lỗi để kiểm tra những gì xảy ra nhằm có được một mô hình tinh thần 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 bằng Firestore.

  1. Trong tệp lib/guest_book.dart, hãy thêm nội dung nhập sau:
import 'guest_book_message.dart';
  1. Trong tiện ích GuestBook, hãy thêm một danh sách các thông báo trong quá trình thiết lập để 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();
}
  1. Trong _GuestBookState, hãy sửa đổi phương thức build 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 một tiện ích Column, sau đó thêm một collection for vào cuối các thành phần con của Column để tạo một Paragraph mới cho mỗi thông báo trong danh sách thông báo.

  1. Cập nhật nội dung của HomePage để tạo GuestBook một cách chính xác 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 tra tính năng đồng bộ hoá tin nhắn

Firestore tự động và ngay lập tức đồng bộ hoá dữ liệu với những ứng dụng khách đã đăng ký cơ sở dữ liệu.

Kiểm tra tính năng đồng bộ hoá tin nhắn:

  1. Trong ứng dụng, hãy tìm những thông báo mà bạn đã tạo trước đó trong cơ sở dữ liệu.
  2. Soạn tin nhắn mới. Chúng xuất hiện ngay lập tức.
  3. Mở không gian làm việc của bạn trong nhiều cửa sổ hoặc thẻ. Các tin nhắn được đồng bộ hoá theo thời gian thực trên các cửa sổ và thẻ.
  4. 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 xuất hiện trong giao diện người dùng.

Xin chúc mừng! Bạn đọc các tài liệu Firestore trong ứng dụng của mình!

Bản xem trước ứng dụng

Màn hình chính của ứng dụng trên Android có tích hợp tính năng trò chuyện

Màn hình chính của ứng dụng trên iOS có tích hợp tính năng trò chuyện

Màn hình chính của ứng dụng trên web có tích hợp tính năng trò chuyện

Màn hình chính của ứng dụng trên macOS có tích hợp tính năng trò chuyện

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ử. Điều này có nghĩa 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ế độ kiểm thử trong giai đoạn đầu của quá trình phát triển. Tốt 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.

Các 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à bộ sưu tậ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 khớp với mọi thứ, từ tất cả các thao tác 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:

  1. 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à một cảnh báo về việc các quy tắc này là công khai:

7767a2d2e64e7275.png

  1. Xác định những bộ sưu tập mà ứng dụng ghi dữ liệu:

Trong match /databases/{database}/documents, hãy xác định bộ sưu tậ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.
  }
}

Vì bạn đã dùng UID xác thực làm trường trong mỗi tài liệu sổ lưu bút, nên bạn có thể lấy 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 trùng khớp.

  1. Thêm quy tắc đọc và ghi vào bộ quy tắc của bạn:
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;
    }
  }
}

Hiện tại, chỉ những người dùng đã đăng nhập mới có thể đọc tin nhắn trong sổ khách, nhưng chỉ tác giả của tin nhắn mới có thể chỉnh sửa tin nhắn.

  1. 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 /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 bổ sung: 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 có người đang đế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 và cho mọi người biết số lượng người sẽ đến. Bạn thêm một số chức năng vào trạng thái ứng dụng. Thứ nhất là khả năng cho phép người dùng đã đăng nhập chỉ định xem họ có tham dự hay không. Thứ hai là bộ đếm số người tham dự.

  1. Trong tệp lib/app_state.dart, hãy thêm các dòng sau vào mục công cụ truy cập của ApplicationState để 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});
  }
}
  1. Cập nhật phương thức init() của ApplicationState 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ố người tham dự và một truy vấn thứ hai chỉ hoạt động khi người dùng đăng nhập để xác định xem người dùng có đang tham dự hay không.

  1. Thêm chế độ liệt kê sau vào đầu tệp lib/app_state.dart.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. 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ư các 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'),
              ),
            ],
          ),
        );
    }
  }
}

Trạng thái này bắt đầu ở trạng thái không xác định, không chọn cũng như Không. Sau khi người dùng chọn có tham dự hay không, bạn sẽ cho thấy lựa chọn đó được làm nổi bật bằng một nút có nền và lựa chọn còn lại sẽ có một nút phẳng.

  1. Cập nhật phương thức build() của HomePage để tận dụng YesNoSelection, cho phép người dùng đã đăng nhập đề cử xem họ có tham dự hay không và hiển thị số lượng 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, nên 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 thêm nội dung vào bộ sưu tập attendees.

  1. Trong bộ sưu tậ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ằng uid 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;
    }
  }
}

Điều 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ư ở đó, nhưng chỉ nhà sáng tạo mới có thể cập nhật danh sách đó.

  1. 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;

    }
  }
}
  1. Không bắt buộc: Trong ứng dụng, hãy nhấp vào các nút để xem kết quả trong trang tổng quan Firestore trên bảng điều khiển của Firebase.

Bản xem trước ứng dụng

Màn hình chính của ứng dụng trên Android

Màn hình chính của ứng dụng trên iOS

Màn hình chính của ứng dụng trên web

Màn hình chính của ứng dụng trên macOS

10. Xin chúc mừng!

Bạn đã dùng Firebase để tạo một ứng dụng web tương tác theo thời gian thực!

Tìm hiểu thêm