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

1. ก่อนเริ่มต้น

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

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

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

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

หน้าจอหลักของแอปใน Android

หน้าจอหลักของแอปใน iOS

สิ่งที่ต้องมี

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

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

นอกจากนี้ คุณยังต้องมีสิ่งต่อไปนี้ด้วย

  • เบราว์เซอร์ที่คุณเลือก เช่น Google Chrome
  • IDE หรือเครื่องมือแก้ไขข้อความที่คุณต้องการกำหนดค่าด้วยปลั๊กอินของ Dart และ Flutter เช่น Android Studio หรือ โค้ด Visual Studio
  • 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 โค้ดสำหรับ Codelab นี้อยู่ในไดเรกทอรี flutter-codelabs/firebase-get-to-know-flutter ไดเรกทอรีจะมีชุดของสแนปชอตที่แสดงให้เห็นว่าโปรเจ็กต์ควรมีลักษณะอย่างไรในตอนท้ายของแต่ละขั้นตอน ตัวอย่างเช่น คุณอยู่ในขั้นตอนที่ 2

  1. ค้นหาไฟล์ที่ตรงกันสำหรับขั้นตอนที่ 2 ดังนี้
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. ในช่องชื่อโปรเจ็กต์ ให้ป้อน Firebase-Flutter-Codelab แล้วคลิกต่อไป

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

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

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

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

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

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

58e3e3e23c2f16a4.png

เปิดใช้ Firestore

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

เปิดใช้ Firestore:

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

99e8429832d23fa3.png

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

6be00e26c72ea032.png

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

278656eefcfb0216.png

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

4. กำหนดค่า Firebase

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

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

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

กำหนดค่าทรัพยากร Dependency

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

  • จากบรรทัดคำสั่ง ให้เพิ่มทรัพยากร Dependency ต่อไปนี้
$ 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. เพิ่มฟังก์ชันการตอบกลับ

เมื่อเพิ่ม 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.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
    );
  }
}

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

  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. แตะปุ่มตอบกลับในแอปเพื่อเริ่มSignInScreen

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

4ed811a25b0cf816.png

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

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

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

โมเดลข้อมูล

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

7c20dc8424bb1d84.png

เพิ่มข้อความใน Firestore

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

  1. สร้างไฟล์ใหม่ชื่อ guest_book.dart เพิ่มวิดเจ็ตการเก็บสถานะ 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'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

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

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

  1. แก้ไขเนื้อหาของ HomePage เพื่อเพิ่ม 2 บรรทัดต่อไปนี้ที่ท้ายรายการย่อยของ 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 ที่มีการผสานรวมแชท

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

โปรดทราบว่า FirebaseAuth.instance.currentUser.uid เป็นการอ้างอิงไปยังรหัสที่ไม่ซ้ำกันซึ่งการตรวจสอบสิทธิ์สร้างขึ้นโดยอัตโนมัติสำหรับผู้ใช้ที่ลงชื่อเข้าสู่ระบบทุกคน

  • เพิ่มเมธอด addMessageToGuestBook ในไฟล์ lib/app_state.dart คุณสามารถเชื่อมต่อความสามารถนี้กับอินเทอร์เฟซผู้ใช้ในขั้นตอนถัดไป

lib/app_state.dart

import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // ...to here.
}

เชื่อมต่อ 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.
        ],
      ),
    );
  }
}

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

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

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

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

713870af0b3b63c.png

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

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

ซิงค์ข้อมูลข้อความ

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

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

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

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

  1. ในไฟล์ lib/app_state.dart ให้เพิ่มบรรทัดต่อไปนี้ลงในส่วนตัวเข้าถึงของ 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) {
        _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();
    });
  }

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

  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 เพื่อสร้างเว็บแอปแบบอินเทอร์แอกทีฟแบบเรียลไทม์

ดูข้อมูลเพิ่มเติม