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

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

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

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

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

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

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

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

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

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

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

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

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

2. รับโค้ดตัวอย่าง

ดาวน์โหลดโปรเจ็กต์เวอร์ชันเริ่มต้นจาก GitHub โดยทำดังนี้

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

ไดเรกทอรี flutter-codelabs มีโค้ดสำหรับชุดของโค้ดแล็บ โค้ดสำหรับโค้ดแล็บนี้อยู่ในไดเรกทอรี 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 ที่ต้องการ ไดเรกทอรีนี้มีโค้ดเริ่มต้นสำหรับโค้ดแล็บ ซึ่งประกอบด้วยแอป Flutter Meetup ที่ยังไม่พร้อมใช้งาน

ค้นหาไฟล์ที่ต้องดำเนินการ

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

  • ค้นหาไฟล์ต่อไปนี้
    • 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 โดยใช้บัญชี Google
  2. คลิกปุ่มเพื่อสร้างโปรเจ็กต์ใหม่ แล้วป้อนชื่อโปรเจ็กต์ (เช่น Firebase-Flutter-Codelab)
  3. คลิกต่อไป
  4. หากได้รับแจ้ง ให้อ่านและยอมรับข้อกำหนดของ Firebase แล้วคลิกต่อไป
  5. (ไม่บังคับ) เปิดใช้ความช่วยเหลือจาก AI ในคอนโซล Firebase (เรียกว่า "Gemini ใน Firebase")
  6. สำหรับ Codelab นี้ คุณไม่จำเป็นต้องใช้ Google Analytics ดังนั้นให้ปิดตัวเลือก Google Analytics
  7. คลิกสร้างโปรเจ็กต์ รอให้ระบบจัดสรรโปรเจ็กต์ แล้วคลิกดำเนินการต่อ

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

ตั้งค่าผลิตภัณฑ์ Firebase

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

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

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

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

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

58e3e3e23c2f16a4.png

ตั้งค่า Firestore

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

วิธีตั้งค่า Firestore ในโปรเจ็กต์ Firebase มีดังนี้

  1. ในแผงด้านซ้ายของคอนโซล Firebase ให้ขยายสร้าง แล้วเลือกฐานข้อมูล Firestore
  2. คลิกสร้างฐานข้อมูล
  3. ตั้งค่ารหัสฐานข้อมูลเป็น (default) ไว้ดังเดิม
  4. เลือกตำแหน่งสำหรับฐานข้อมูล แล้วคลิกถัดไป
    สำหรับแอปจริง คุณควรเลือกตำแหน่งที่อยู่ใกล้กับผู้ใช้
  5. คลิกเริ่มในโหมดทดสอบ อ่านข้อจำกัดความรับผิดเกี่ยวกับกฎความปลอดภัย
    ในภายหลังใน Codelab นี้ คุณจะเพิ่มกฎความปลอดภัยเพื่อรักษาความปลอดภัยของข้อมูล อย่าเผยแพร่หรือเปิดเผยแอปต่อสาธารณะโดยไม่ได้เพิ่มกฎความปลอดภัยสำหรับฐานข้อมูล
  6. คลิกสร้าง

4. กำหนดค่า Firebase

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

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

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

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

คุณต้องเพิ่มไลบรารี FlutterFire สำหรับผลิตภัณฑ์ Firebase 2 รายการที่คุณใช้ในแอปนี้ ได้แก่ Authentication และ 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

คุณได้เพิ่มแพ็กเกจที่จำเป็นแล้ว แต่ยังต้องกำหนดค่าโปรเจ็กต์ Runner ของ iOS, Android, macOS และเว็บเพื่อให้ใช้ 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 ลงในแอปแล้ว คุณสามารถสร้างปุ่มตอบรับคำเชิญที่ลงทะเบียนผู้ใช้ด้วยการตรวจสอบสิทธิ์ สำหรับ 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 วิดเจ็ต Consumer เป็นวิธีปกติที่ใช้provider package เพื่อสร้างส่วนหนึ่งของแผนผังใหม่เมื่อสถานะแอปเปลี่ยนแปลง วิดเจ็ต 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 เป็นการอ้างอิงถึงรหัสที่ไม่ซ้ำที่สร้างขึ้นโดยอัตโนมัติซึ่งการตรวจสอบสิทธิ์จะให้แก่ผู้ใช้ที่เข้าสู่ระบบทั้งหมด

  • ในไฟล์ 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 ตอนนี้สิ่งที่คุณต้องทำก็คือเชื่อมต่อทั้ง 2 อย่าง

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

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

  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. อัปเดตวิธีการของ HomePagebuild() เพื่อใช้ประโยชน์จาก 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 เพื่อสร้างเว็บแอปแบบเรียลไทม์ที่มีการโต้ตอบได้

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