ทำความรู้จัก Firebase สำหรับ Flutter

1. ก่อนเริ่ม

ใน Codelab นี้ คุณจะได้เรียนรู้พื้นฐานบางอย่างของ Firebase เพื่อสร้างแอปมือถือ Flutter สำหรับ Android และ iOS

ข้อกำหนดเบื้องต้น

สิ่งที่คุณจะได้เรียนรู้

  • วิธีสร้างแอปแชท RSVP และสมุดเยี่ยมบน Android, iOS, เว็บและ macOS ด้วย Flutter
  • วิธีตรวจสอบผู้ใช้ด้วย Firebase Authentication และซิงค์ข้อมูลกับ Firestore

หน้าจอหลักของแอพบน Android

หน้าจอหลักของแอพบน iOS

สิ่งที่คุณต้องการ

อุปกรณ์ใด ๆ ต่อไปนี้:

  • อุปกรณ์ Android หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์ของคุณและตั้งค่าเป็นโหมดนักพัฒนาซอฟต์แวร์
  • โปรแกรมจำลอง iOS (ต้องใช้ เครื่องมือ Xcode )
  • โปรแกรมจำลอง Android (ต้องตั้งค่าใน Android Studio )

คุณต้องมีสิ่งต่อไปนี้ด้วย:

  • เบราว์เซอร์ที่คุณเลือก เช่น Google Chrome
  • IDE หรือโปรแกรมแก้ไขข้อความที่คุณเลือกกำหนดค่าด้วยปลั๊กอิน Dart และ Flutter เช่น Android Studio หรือ Visual Studio Code
  • Flutter หรือ beta เวอร์ชัน stable ล่าสุดหากคุณชอบใช้ชีวิตบนขอบ
  • บัญชี Google สำหรับสร้างและจัดการโครงการ Firebase ของคุณ
  • Firebase CLI ลงชื่อเข้าใช้บัญชี Google ของคุณ

2. รับรหัสตัวอย่าง

ดาวน์โหลดเวอร์ชันเริ่มต้นของโครงการของคุณจาก GitHub:

  1. จากบรรทัดคำสั่ง ให้โคลน ที่เก็บ GitHub ในไดเร็กทอรี flutter-codelabs :
git clone https://github.com/flutter/codelabs.git flutter-codelabs

ไดเร็กทอรี flutter-codelabs มีโค้ดสำหรับชุดโค้ดแล็บ รหัสสำหรับ codelab นี้อยู่ในไดเร็กทอรี flutter-codelabs/firebase-get-to-know-flutter ไดเร็กทอรีประกอบด้วยชุดสแนปชอตที่แสดงว่าโปรเจ็กต์ของคุณควรมีลักษณะอย่างไรเมื่อสิ้นสุดแต่ละขั้นตอน ตัวอย่างเช่น คุณอยู่ในขั้นตอนที่สอง

  1. ค้นหาไฟล์ที่ตรงกันสำหรับขั้นตอนที่สอง:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

ถ้าคุณต้องการข้ามไปข้างหน้าหรือดูว่าบางอย่างควรมีลักษณะอย่างไรหลังจากขั้นตอนหนึ่งๆ ให้ดูในไดเร็กทอรีที่ตั้งชื่อตามขั้นตอนที่คุณสนใจ

นำเข้าแอปเริ่มต้น

  • เปิดหรือนำเข้าไดเร็กทอรี flutter-codelabs/firebase-get-to-know-flutter/step_02 ใน IDE ที่คุณต้องการ ไดเร็กทอรีนี้มีโค้ดเริ่มต้นสำหรับ codelab ซึ่งประกอบด้วยแอพพบปะ 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 มีดังนี้

หน้าจอหลักของแอพบน Android

หน้าจอหลักของแอพบน iOS

หน้าจอหลักของแอปบนเว็บ

หน้าจอหลักของแอพบน macOS

3. สร้างและกำหนดค่าโครงการ Firebase

การแสดงข้อมูลกิจกรรมนั้นยอดเยี่ยมสำหรับแขกของคุณ แต่ก็ไม่มีประโยชน์มากนักสำหรับใครก็ตาม คุณต้องเพิ่มฟังก์ชันไดนามิกบางอย่างให้กับแอป ในการทำเช่นนั้น คุณต้องเชื่อมต่อ Firebase กับแอปของคุณ ในการเริ่มต้นใช้งาน Firebase คุณต้องสร้างและกำหนดค่าโปรเจ็กต์ Firebase

สร้างโครงการ Firebase

  1. ลงชื่อเข้าใช้ Firebase
  2. ในคอนโซล ให้คลิก เพิ่มโครงการ หรือ สร้างโครงการ
  3. ในช่อง Project name ให้ป้อน Firebase-Flutter-Codelab แล้วคลิก Continue

4395e4e67c08043a.png

  1. คลิกผ่านตัวเลือกการสร้างโครงการ หากได้รับแจ้ง ให้ยอมรับข้อกำหนดของ Firebase แต่ข้ามการตั้งค่า Google Analytics เนื่องจากคุณจะไม่ใช้ Google Analytics สำหรับแอปนี้

b7138cde5f2c7b61.png

หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับโปรเจ็กต์ Firebase โปรดดู ที่ ทำความเข้าใจโปรเจ็กต์ Firebase

แอปใช้ผลิตภัณฑ์ Firebase ต่อไปนี้ซึ่งมีให้สำหรับเว็บแอป:

  • การรับรองความถูกต้อง: อนุญาตให้ผู้ใช้ลงชื่อเข้าใช้แอปของคุณ
  • Firestore: บันทึกข้อมูลที่มีโครงสร้างบนคลาวด์และรับการแจ้งเตือนทันทีเมื่อข้อมูลมีการเปลี่ยนแปลง
  • กฎความปลอดภัยของ Firebase: รักษาความปลอดภัยฐานข้อมูลของคุณ

ผลิตภัณฑ์เหล่านี้บางรายการต้องมีการกำหนดค่าพิเศษ หรือคุณต้องเปิดใช้งานในคอนโซล Firebase

เปิดใช้งานการตรวจสอบสิทธิ์การลงชื่อเข้าใช้ด้วยอีเมล

  1. ในบาน หน้าต่างภาพรวมโครงการ ของคอนโซล Firebase ให้ขยายเมนู สร้าง
  2. คลิก การรับรองความถูกต้อง > เริ่มต้น > วิธีการลงชื่อเข้าใช้ > อีเมล/รหัสผ่าน > เปิดใช้งาน > บันทึก

58e3e3e23c2f16a4.png

เปิดใช้งาน Firestore

เว็บแอปใช้ Firestore เพื่อบันทึกข้อความแชทและรับข้อความแชทใหม่

เปิดใช้งาน Firestore:

  • ในเมนู สร้าง ให้ คลิก Cloud Firestore > สร้างฐานข้อมูล

99e8429832d23fa3.png

  1. เลือก เริ่มในโหมดทดสอบ จากนั้นอ่านข้อจำกัดความรับผิดชอบเกี่ยวกับกฎความปลอดภัย โหมดทดสอบช่วยให้แน่ใจว่าคุณสามารถเขียนลงในฐานข้อมูลระหว่างการพัฒนาได้อย่างอิสระ

6be00e26c72ea032.png

  1. คลิก ถัดไป จากนั้นเลือกตำแหน่งที่ตั้งสำหรับฐานข้อมูลของคุณ คุณสามารถใช้ค่าเริ่มต้น คุณไม่สามารถเปลี่ยนตำแหน่งได้ในภายหลัง

278656eefcfb0216.png

  1. คลิก เปิดใช้งาน

4. กำหนดค่า Firebase

หากต้องการใช้ Firebase กับ Flutter คุณต้องทำงานต่อไปนี้ให้เสร็จเพื่อกำหนดค่าโปรเจ็กต์ Flutter เพื่อใช้ไลบรารี FlutterFire อย่างถูกต้อง:

  1. เพิ่มการอ้างอิง FlutterFire ในโครงการของคุณ
  2. ลงทะเบียนแพลตฟอร์มที่ต้องการในโครงการ Firebase
  3. ดาวน์โหลดไฟล์คอนฟิกูเรชันเฉพาะแพลตฟอร์ม จากนั้นเพิ่มลงในโค้ด

ในไดเร็กทอรีระดับบนสุดของแอพ Flutter มีไดเร็กทอรีย่อย android , ios , macos และ web ซึ่งเก็บไฟล์การกำหนดค่าเฉพาะแพลตฟอร์มสำหรับ iOS และ Android ตามลำดับ

กำหนดค่าการพึ่งพา

คุณต้องเพิ่มไลบรารี FlutterFire สำหรับผลิตภัณฑ์ Firebase สองรายการที่คุณใช้ในแอปนี้: การรับรองความถูกต้องและ Firestore

  • จากบรรทัดรับคำสั่ง เพิ่มการอ้างอิงต่อไปนี้:
$ flutter pub add firebase_core

แพ็คเกจ firebase_core เป็นรหัสทั่วไปที่จำเป็นสำหรับปลั๊กอิน Firebase Flutter ทั้งหมด

$ flutter pub add firebase_auth

แพ็คเกจ firebase_auth เปิดใช้งานการรวมกับการรับรองความถูกต้อง

$ flutter pub add cloud_firestore

แพ็คเกจ cloud_firestore ช่วยให้สามารถเข้าถึงที่จัดเก็บข้อมูล Firestore

$ flutter pub add provider

แพ็คเกจ firebase_ui_auth มีชุดของวิดเจ็ตและยูทิลิตี้เพื่อเพิ่มความเร็วของนักพัฒนาด้วยขั้นตอนการตรวจสอบสิทธิ์

$ flutter pub add firebase_ui_auth

คุณเพิ่มแพ็คเกจที่จำเป็นแล้ว แต่คุณต้องกำหนดค่าโครงการ iOS, Android, macOS และ Web Runner เพื่อใช้ Firebase อย่างเหมาะสม คุณยังใช้ แพ็คเกจ provider ที่เปิดใช้งานการแยกตรรกะทางธุรกิจออกจากตรรกะการแสดงผล

ติดตั้ง FlutterFire CLI

FlutterFire CLI ขึ้นอยู่กับ Firebase CLI พื้นฐาน

  1. หากคุณยังไม่ได้ติดตั้ง ให้ติดตั้ง Firebase CLI บนเครื่องของคุณ
  2. ติดตั้ง FlutterFire CLI:
$ dart pub global activate flutterfire_cli

เมื่อติดตั้งแล้ว คำสั่ง flutterfire จะพร้อมใช้งานทั่วโลก

กำหนดค่าแอปของคุณ

CLI ดึงข้อมูลจากโปรเจ็กต์ Firebase และแอปโปรเจ็กต์ที่เลือกเพื่อสร้างการกำหนดค่าทั้งหมดสำหรับแพลตฟอร์มเฉพาะ

ในรูทของแอป ให้รันคำสั่ง configure :

$ flutterfire configure

คำสั่งคอนฟิกูเรชันจะแนะนำคุณตลอดกระบวนการต่อไปนี้:

  1. เลือกโปรเจ็กต์ Firebase ตามไฟล์ .firebaserc หรือจากคอนโซล Firebase
  2. กำหนดแพลตฟอร์มสำหรับการกำหนดค่า เช่น Android, iOS, macOS และเว็บ
  3. ระบุแอป Firebase ที่จะใช้แยกการกำหนดค่า โดยค่าเริ่มต้น CLI จะพยายามจับคู่แอป Firebase โดยอัตโนมัติตามการกำหนดค่าโปรเจ็กต์ปัจจุบันของคุณ
  4. สร้างไฟล์ firebase_options.dart ในโครงการของคุณ

กำหนดค่า macOS

Flutter บน macOS สร้างแอพแซนด์บ็อกซ์เต็มรูปแบบ เนื่องจากแอปนี้รวมเข้ากับเครือข่ายเพื่อสื่อสารกับเซิร์ฟเวอร์ Firebase คุณต้องกำหนดค่าแอปของคุณด้วยสิทธิ์ของไคลเอ็นต์เครือข่าย

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

สำหรับข้อมูลเพิ่มเติม โปรดดู การสนับสนุนเดสก์ท็อปสำหรับ Flutter

5. เพิ่มฟังก์ชัน RSVP

เมื่อคุณเพิ่ม Firebase ลงในแอปแล้ว คุณสามารถสร้างปุ่ม RSVP ที่ลงทะเบียนบุคคลด้วย การรับรองความถูกต้อง สำหรับ Android ดั้งเดิม iOS ดั้งเดิม และเว็บ มีแพ็คเกจ FirebaseUI Auth ที่สร้างไว้ล่วงหน้า แต่คุณต้องสร้างความสามารถนี้สำหรับ Flutter

โปรเจ็กต์ที่คุณเรียกใช้ก่อนหน้านี้มีชุดของวิดเจ็ตที่ใช้อินเทอร์เฟซผู้ใช้สำหรับโฟลว์การพิสูจน์ตัวตนส่วนใหญ่ คุณใช้ตรรกะทางธุรกิจเพื่อรวมการรับรองความถูกต้องกับแอป

เพิ่มตรรกะทางธุรกิจด้วยแพ็คเกจ Provider

ใช้ แพ็คเกจ provider เพื่อสร้างวัตถุสถานะแอพส่วนกลางให้ใช้งานได้ทั่วทั้งแผนผังวิดเจ็ต Flutter ของแอพ:

  1. สร้างไฟล์ใหม่ชื่อ 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 นี้มีหน้าที่รับผิดชอบหลักประการหนึ่งสำหรับขั้นตอนนี้ ซึ่งก็คือการเตือนแผนผังวิดเจ็ตว่ามีการอัพเดตเป็นสถานะที่พิสูจน์ตัวตนแล้ว

คุณใช้ผู้ให้บริการเพื่อสื่อสารสถานะการเข้าสู่ระบบของผู้ใช้ไปยังแอปเท่านั้น ในการให้ผู้ใช้เข้าสู่ระบบ ให้ใช้ UI ที่มาจากแพ็คเกจ firebase_ui_auth ซึ่งเป็นวิธีที่ยอดเยี่ยมในการบูตหน้าจอเข้าสู่ระบบในแอปของคุณอย่างรวดเร็ว

รวมขั้นตอนการตรวจสอบสิทธิ์

  1. แก้ไขการอิมพอร์ตที่ด้านบนของไฟล์ 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. เชื่อมต่อสถานะแอปกับการเริ่มต้นแอป จากนั้นเพิ่มขั้นตอนการตรวจสอบสิทธิ์ไปยัง HomePage :

lib/main.dart

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

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

การแก้ไขฟังก์ชัน main() ทำให้แพ็คเกจผู้ให้บริการรับผิดชอบการสร้างอินสแตนซ์ของออบเจกต์สถานะแอปด้วยวิดเจ็ต ChangeNotifierProvider คุณใช้คลาส provider เฉพาะนี้เนื่องจากวัตถุสถานะแอปขยายคลาส ChangeNotifier ซึ่งช่วยให้แพ็คเกจ provider รู้ว่าเมื่อใดควรแสดงวิดเจ็ตที่ขึ้นต่อกันอีกครั้ง

  1. อัปเดตแอปของคุณเพื่อจัดการการนำทางไปยังหน้าจอต่างๆ ที่ FirebaseUI มีให้คุณ โดยสร้างการกำหนดค่า GoRouter :

lib/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.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
    );
  }
}

แต่ละหน้าจอมีการดำเนินการประเภทต่างๆ ที่เกี่ยวข้องโดยอิงตามสถานะใหม่ของโฟลว์การรับรองความถูกต้อง หลังจากเปลี่ยนสถานะส่วนใหญ่ในการรับรองความถูกต้องแล้ว คุณสามารถเปลี่ยนเส้นทางกลับไปยังหน้าจอที่ต้องการได้ ไม่ว่าจะเป็นหน้าจอหลักหรือหน้าจออื่น เช่น โปรไฟล์

  1. ในวิธีการสร้างของคลาส 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 วิดเจ็ตสำหรับผู้บริโภคเป็นวิธีปกติที่สามารถใช้แพ็คเกจ provider เพื่อสร้างส่วนของแผนผังใหม่เมื่อสถานะของแอพเปลี่ยนไป วิดเจ็ต AuthFunc เป็นวิดเจ็ตเสริมที่คุณทดสอบ

ทดสอบโฟลว์การรับรองความถูกต้อง

cdf2d25e436bd48d.png

  1. ในแอป ให้แตะปุ่ม RSVP เพื่อเริ่มต้น SignInScreen

2a2cd6d69d172369.png

  1. ป้อนที่อยู่อีเมล หากคุณลงทะเบียนแล้ว ระบบจะให้คุณป้อนรหัสผ่าน มิฉะนั้น ระบบจะแจ้งให้คุณกรอกแบบฟอร์มลงทะเบียน

e5e65065dba36b54.png

  1. ป้อนรหัสผ่านที่มีความยาวน้อยกว่าหกอักขระเพื่อตรวจสอบขั้นตอนการจัดการข้อผิดพลาด หากคุณลงทะเบียนแล้ว คุณจะเห็นรหัสผ่านแทน
  2. ป้อนรหัสผ่านไม่ถูกต้องเพื่อตรวจสอบขั้นตอนการจัดการข้อผิดพลาด
  3. ป้อนรหัสผ่านที่ถูกต้อง คุณจะเห็นประสบการณ์การเข้าสู่ระบบ ซึ่งให้ผู้ใช้สามารถออกจากระบบได้

4ed811a25b0cf816.png

6. เขียนข้อความไปที่ Firestore

เป็นเรื่องดีที่ทราบว่าผู้ใช้กำลังจะมา แต่คุณต้องให้แขกทำอย่างอื่นในแอป จะเกิดอะไรขึ้นถ้าพวกเขาสามารถฝากข้อความไว้ในสมุดเยี่ยมได้? พวกเขาสามารถแบ่งปันว่าทำไมพวกเขาถึงรู้สึกตื่นเต้นที่ได้มาหรือใครที่พวกเขาหวังว่าจะได้พบ

หากต้องการจัดเก็บข้อความแชทที่ผู้ใช้เขียนในแอป ให้ใช้ Firestore

แบบจำลองข้อมูล

Firestore เป็นฐานข้อมูล NoSQL และข้อมูลที่จัดเก็บไว้ในฐานข้อมูลจะแบ่งออกเป็นคอลเล็กชัน เอกสาร ฟิลด์ และคอลเล็กชันย่อย คุณจัดเก็บแต่ละข้อความของการแชทเป็นเอกสารในคอ guestbook ชันสมุดเยี่ยม ซึ่งเป็นคอลเลกชันระดับบนสุด

7c20dc8424bb1d84.png

เพิ่มข้อความไปยัง Firestore

ในส่วนนี้ คุณจะเพิ่มฟังก์ชันสำหรับผู้ใช้ในการเขียนข้อความไปยังฐานข้อมูล ขั้นแรก คุณเพิ่มฟิลด์แบบฟอร์มและปุ่มส่ง จากนั้นเพิ่มรหัสที่เชื่อมต่อองค์ประกอบเหล่านี้กับฐานข้อมูล

  1. สร้างไฟล์ใหม่ชื่อ guest_book.dart เพิ่มวิดเจ็ต Stateful GuestBook เพื่อสร้างองค์ประกอบ UI ของช่องข้อความและปุ่มส่ง:

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 เติมช่องว่างเพิ่มเติมในแถว เพื่อให้เข้าใจได้ดีขึ้นว่าเหตุใดจึงต้องใช้สิ่งนี้ โปรดดูที่ การทำความเข้าใจข้อจำกัด

ตอนนี้คุณมีวิดเจ็ตที่ช่วยให้ผู้ใช้สามารถป้อนข้อความเพื่อเพิ่มลงในสมุดเยี่ยมได้ คุณต้องนำวิดเจ็ตนั้นมาแสดงบนหน้าจอ

  1. แก้ไขเนื้อหาของ 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)),

แม้ว่านี่จะเพียงพอที่จะแสดงวิดเจ็ต แต่ก็ไม่เพียงพอที่จะทำอะไรที่เป็นประโยชน์ คุณอัปเดตรหัสนี้ในไม่ช้าเพื่อให้ใช้งานได้

ดูตัวอย่างแอป

หน้าจอหลักของแอพบน Android พร้อมการรวมแชท

หน้าจอหลักของแอพบน iOS พร้อมการรวมแชท

หน้าจอหลักของแอปบนเว็บที่มีการรวมการแชท

หน้าจอหลักของแอพบน macOS พร้อมการรวมแชท

เมื่อผู้ใช้คลิก SEND จะเรียกใช้ข้อมูลโค้ดต่อไปนี้ เพิ่มเนื้อหาของฟิลด์ป้อนข้อความไปยังคอลเลกชัน guestbook ของฐานข้อมูล โดยเฉพาะอย่างยิ่ง เมธอด addMessageToGuestBook เพิ่มเนื้อหาข้อความไปยังเอกสารใหม่ด้วย ID ที่สร้างขึ้นโดยอัตโนมัติในคอลเลกชัน guestbook

โปรดทราบว่า FirebaseAuth.instance.currentUser.uid เป็นการอ้างอิงถึง ID เฉพาะที่สร้างขึ้นโดยอัตโนมัติซึ่งการรับรองความถูกต้องมอบให้กับผู้ใช้ที่เข้าสู่ระบบทั้งหมด

  • ในไฟล์ 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.
}

เชื่อมต่อ UI และฐานข้อมูล

คุณมี UI ที่ผู้ใช้สามารถป้อนข้อความที่ต้องการเพิ่มในสมุดเยี่ยม และคุณมีโค้ดสำหรับเพิ่มรายการใน 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> อีกครั้งเพื่อทำให้สถานะแอปพร้อมใช้งานสำหรับส่วนของแผนผังที่คุณแสดงผล ซึ่งช่วยให้คุณโต้ตอบกับบุคคลที่ป้อนข้อความใน UI และเผยแพร่ในฐานข้อมูล ในส่วนถัดไป คุณจะทดสอบว่าข้อความที่เพิ่มนั้นเผยแพร่ในฐานข้อมูลหรือไม่

ทดสอบการส่งข้อความ

  1. หากจำเป็น ให้ลงชื่อเข้าใช้แอป
  2. ป้อนข้อความ เช่น Hey there! แล้วคลิก ส่ง

การดำเนินการนี้เขียนข้อความไปยังฐานข้อมูล Firestore ของคุณ อย่างไรก็ตาม คุณไม่เห็นข้อความในแอป Flutter จริงของคุณ เนื่องจากคุณยังคงต้องใช้การดึงข้อมูล ซึ่งคุณจะดำเนินการในขั้นตอนต่อไป อย่างไรก็ตาม ใน แดชบอร์ด ฐานข้อมูล ของคอนโซล Firebase คุณจะเห็นข้อความที่เพิ่มไว้ในคอลเล็กชัน guestbook ถ้าคุณส่งข้อความมากขึ้น แสดงว่าคุณเพิ่มเอกสารในคอลเลคชัน guestbook ของคุณ ตัวอย่างเช่น ดูข้อมูลโค้ดต่อไปนี้:

713870af0b3b63c.png

7. อ่านข้อความ

เป็นเรื่องดีที่ผู้เยี่ยมชมสามารถเขียนข้อความไปยังฐานข้อมูล แต่ยังไม่เห็นข้อความเหล่านั้นในแอป ได้เวลาแก้ไขแล้ว!

ซิงโครไนซ์ข้อความ

หากต้องการแสดงข้อความ คุณต้องเพิ่มผู้ฟังที่จะทริกเกอร์เมื่อข้อมูลเปลี่ยนแปลง แล้วจึงสร้างองค์ประกอบ UI ที่แสดงข้อความใหม่ คุณเพิ่มโค้ดลงในสถานะแอปที่รับฟังข้อความที่เพิ่มใหม่จากแอป

  1. สร้างไฟล์ใหม่ 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;
}
  1. ในไฟล์ 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
  1. ในส่วนของ ApplicationState ที่คุณกำหนด state และ getters ให้เพิ่มบรรทัดต่อไปนี้:

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. ในส่วนการเริ่มต้นของ 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

  1. ในไฟล์ lib/guest_book.dart ให้เพิ่มการอิมพอร์ตต่อไปนี้:
import 'guest_book_message.dart';
  1. ในวิดเจ็ต 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();
}
  1. ใน _GuestBookState แก้ไขวิธี build ดังต่อไปนี้เพื่อแสดงการกำหนดค่านี้:

lib/guest_book.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here...
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // ...to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here...
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // ...to here.
    );
  }
}

คุณรวมเนื้อหาก่อนหน้าของเมธอด build() ด้วยวิดเจ็ต Column จากนั้นเพิ่ม คอลเล็กชันสำหรับ ที่ส่วนท้ายของ Column ย่อยเพื่อสร้าง Paragraph ใหม่สำหรับแต่ละข้อความในรายการข้อความ

  1. อัปเดตเนื้อหาของ 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 ซิงโครไนซ์ข้อมูลกับลูกค้าที่สมัครเป็นสมาชิกฐานข้อมูลโดยอัตโนมัติและทันที

ทดสอบการซิงโครไนซ์ข้อความ:

  1. ในแอป ให้ค้นหาข้อความที่คุณสร้างไว้ก่อนหน้านี้ในฐานข้อมูล
  2. เขียนข้อความใหม่ ปรากฏขึ้นทันที
  3. เปิดพื้นที่ทำงานของคุณในหลายหน้าต่างหรือหลายแท็บ ข้อความจะซิงค์ตามเวลาจริงในหน้าต่างและแท็บต่างๆ
  4. ไม่บังคับ: ในเมนู ฐานข้อมูล ของคอนโซล Firebase ให้ลบ แก้ไข หรือเพิ่มข้อความใหม่ด้วยตนเอง การเปลี่ยนแปลงทั้งหมดปรากฏใน UI

ยินดีด้วย! คุณอ่านเอกสาร Firestore ในแอปของคุณแล้ว!

ดูตัวอย่างแอป

หน้าจอหลักของแอพบน Android พร้อมการรวมแชท

หน้าจอหลักของแอพบน iOS พร้อมการรวมแชท

หน้าจอหลักของแอปบนเว็บที่มีการรวมการแชท

หน้าจอหลักของแอพบน macOS พร้อมการรวมแชท

8. ตั้งกฎความปลอดภัยขั้นพื้นฐาน

เริ่มแรกคุณตั้งค่า Firestore ให้ใช้โหมดทดสอบ ซึ่งหมายความว่าฐานข้อมูลของคุณเปิดให้อ่านและเขียน อย่างไรก็ตาม คุณควรใช้โหมดทดสอบในช่วงเริ่มต้นของการพัฒนาเท่านั้น ตามแนวทางปฏิบัติที่ดีที่สุด คุณควรตั้งกฎความปลอดภัยสำหรับฐานข้อมูลของคุณขณะที่คุณพัฒนาแอป การรักษาความปลอดภัยเป็นองค์ประกอบสำคัญในโครงสร้างและลักษณะการทำงานของแอปของคุณ

กฎความปลอดภัยของ Firebase ให้คุณควบคุมการเข้าถึงเอกสารและคอลเลกชันในฐานข้อมูลของคุณ ไวยากรณ์ของกฎที่ยืดหยุ่นช่วยให้คุณสร้างกฎที่ตรงกับอะไรก็ได้ ตั้งแต่การเขียนทั้งหมดไปยังฐานข้อมูลทั้งหมด ไปจนถึงการดำเนินการในเอกสารเฉพาะ

ตั้งค่ากฎความปลอดภัยพื้นฐาน:

  1. ในเมนู พัฒนา ของคอนโซล Firebase ให้คลิก ฐานข้อมูล > กฎ คุณควรเห็นกฎความปลอดภัยเริ่มต้นต่อไปนี้และคำเตือนเกี่ยวกับกฎที่เป็นสาธารณะ:

7767a2d2e64e7275.png

  1. ระบุคอลเลกชันที่แอปเขียนข้อมูล:

ใน 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 การตรวจสอบสิทธิ์ที่ตรงกัน

  1. เพิ่มกฎการอ่านและเขียนให้กับชุดกฎของคุณ:
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;
    }
  }
}

ขณะนี้ เฉพาะผู้ใช้ที่ลงชื่อเข้าใช้เท่านั้นที่สามารถอ่านข้อความในสมุดเยี่ยมได้ แต่มีเพียงผู้เขียนข้อความเท่านั้นที่สามารถแก้ไขข้อความได้

  1. เพิ่มการตรวจสอบข้อมูลเพื่อให้แน่ใจว่าฟิลด์ที่คาดไว้ทั้งหมดมีอยู่ในเอกสาร:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. ขั้นตอนโบนัส: ฝึกฝนสิ่งที่คุณได้เรียนรู้

บันทึกสถานะ RSVP ของผู้เข้าร่วม

ตอนนี้ แอปของคุณอนุญาตให้ผู้คนแชทเมื่อพวกเขาสนใจในกิจกรรมเท่านั้น นอกจากนี้ วิธีเดียวที่คุณจะรู้ว่ามีคนกำลังมาคือเมื่อพวกเขาพูดเช่นนั้นในแชท

ในขั้นตอนนี้ คุณจะได้จัดระเบียบและแจ้งให้ผู้คนทราบว่าจะมากี่คน คุณเพิ่มความสามารถบางอย่างให้กับสถานะแอป ประการแรกคือความสามารถสำหรับผู้ใช้ที่เข้าสู่ระบบเพื่อเสนอว่าพวกเขากำลังเข้าร่วมหรือไม่ ประการที่สองเป็นตัวนับจำนวนผู้เข้าร่วมประชุม

  1. ในไฟล์ lib/app_state.dart ให้เพิ่มบรรทัดต่อไปนี้ลงในส่วน accessors ของ ApplicationState เพื่อให้โค้ด UI สามารถโต้ตอบกับสถานะนี้ได้:

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. อัปเดตเมธอด 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) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here...
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // ...to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

รหัสนี้เพิ่มแบบสอบถามที่สมัครรับข้อมูลเสมอเพื่อกำหนดจำนวนผู้เข้าร่วมและแบบสอบถามที่สองที่ใช้งานเฉพาะในขณะที่ผู้ใช้เข้าสู่ระบบเพื่อตรวจสอบว่าผู้ใช้เข้าร่วมหรือไม่

  1. เพิ่มการแจงนับต่อไปนี้ที่ด้านบนของไฟล์ lib/app_state.dart

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. สร้างไฟล์ใหม่ 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'),
              ),
            ],
          ),
        );
    }
  }
}

เริ่มต้นในสถานะที่ไม่แน่นอนโดยไม่ได้เลือกทั้ง ใช่ และ ไม่ใช่ เมื่อผู้ใช้เลือกว่าจะเข้าร่วมหรือไม่ คุณจะแสดงตัวเลือกนั้นที่ไฮไลต์ด้วยปุ่มที่กรอก และตัวเลือกอื่นๆ จะลดลงด้วยการแสดงผลแบบเรียบๆ

  1. อัปเดตเมธอด 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

  1. ในคอลเลกชั่น 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;
    }
  }
}

ซึ่งช่วยให้ทุกคนอ่านรายชื่อผู้เข้าร่วมได้เนื่องจากไม่มีข้อมูลส่วนตัวในนั้น แต่มีเพียงผู้สร้างเท่านั้นที่สามารถอัปเดตได้

  1. เพิ่มการตรวจสอบข้อมูลเพื่อให้แน่ใจว่าฟิลด์ที่คาดไว้ทั้งหมดมีอยู่ในเอกสาร:
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. ไม่บังคับ: ในแอป ให้คลิกปุ่มเพื่อดูผลลัพธ์ในแดชบอร์ด Firestore ในคอนโซล Firebase

ดูตัวอย่างแอป

หน้าจอหลักของแอพบน Android

หน้าจอหลักของแอพบน iOS

หน้าจอหลักของแอปบนเว็บ

หน้าจอหลักของแอพบน macOS

10. ยินดีด้วย!

คุณใช้ Firebase เพื่อสร้างเว็บแอปเชิงโต้ตอบแบบเรียลไทม์!

เรียนรู้เพิ่มเติม