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

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

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้ข้อมูลเบื้องต้นเกี่ยวกับ 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
  • 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 มีโค้ดสำหรับคอลเล็กชัน 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 เนื่องจากคุณจะไม่ได้ใช้ Google Analytics สําหรับแอปนี้

b7138cde5f2c7b61.png

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

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

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

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

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

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

58e3e3e23c2f16a4.png

ตั้งค่า Firestore

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

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

  1. ในแผงด้านซ้ายของคอนโซล Firebase ให้ขยาย Build แล้วเลือกฐานข้อมูล 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

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

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

คุณได้แทนที่ 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 ซึ่งกำหนดรัฐและ 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 ให้เพิ่มบรรทัดต่อไปนี้ลงในส่วน 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) {
        _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 เพื่อสร้างเว็บแอปแบบเรียลไทม์ที่โต้ตอบได้

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