ทำความรู้จัก 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 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 ที่ต้องการ ไดเรกทอรีนี้มีโค้ดเริ่มต้นสําหรับโค้ดแล็บ ซึ่งประกอบด้วยแอป Meetup ของ 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 ให้ขยายสร้าง แล้วเลือกฐานข้อมูล Firestore
  2. คลิกสร้างฐานข้อมูล
  3. ตั้งค่ารหัสฐานข้อมูลเป็น (default)
  4. เลือกตำแหน่งสำหรับฐานข้อมูล แล้วคลิกถัดไป
    สำหรับแอปจริง คุณควรเลือกตำแหน่งที่อยู่ใกล้กับผู้ใช้
  5. คลิกเริ่มในโหมดทดสอบ อ่านข้อจำกัดความรับผิดเกี่ยวกับกฎการรักษาความปลอดภัย
    ในภายหลังในโค้ดแล็บนี้ คุณจะเพิ่มกฎการรักษาความปลอดภัยเพื่อรักษาความปลอดภัยให้ข้อมูล อย่าเผยแพร่หรือแสดงแอปต่อสาธารณะโดยไม่เพิ่มกฎความปลอดภัยสําหรับฐานข้อมูล
  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

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

lib/home_page.dart

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

import 'app_state.dart';                          // new
import 'src/authentication.dart';                 // new
import 'src/widgets.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

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

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

cdf2d25e436bd48d.png

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

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

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