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

1. ก่อนที่คุณจะเริ่ม

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • ค้นหาไฟล์ต่อไปนี้:
    • lib/main.dart : ไฟล์นี้มีจุดเริ่มต้นหลักและวิดเจ็ตแอป
    • lib/home_page.dart : ไฟล์นี้มีวิดเจ็ตโฮมเพจ
    • lib/src/widgets.dart : ไฟล์นี้มีวิดเจ็ตจำนวนหนึ่งเพื่อช่วยกำหนดมาตรฐานสไตล์ของแอป พวกเขาสร้างหน้าจอของแอพเริ่มต้น
    • lib/src/authentication.dart : ไฟล์นี้มีการใช้งาน การตรวจสอบสิทธิ์ บางส่วนด้วยชุดวิดเจ็ตเพื่อสร้างประสบการณ์ผู้ใช้เข้าสู่ระบบสำหรับการตรวจสอบสิทธิ์ผ่านอีเมลของ Firebase วิดเจ็ตเหล่านี้สำหรับขั้นตอนการตรวจสอบสิทธิ์ยังไม่ได้ใช้ในแอปเริ่มต้น แต่คุณจะเพิ่มเร็วๆ นี้

คุณเพิ่มไฟล์เพิ่มเติมตามที่จำเป็นเพื่อสร้างส่วนที่เหลือของแอป

ตรวจสอบไฟล์ lib/main.dart

แอปนี้ใช้ประโยชน์จากแพ็คเกจ google_fonts เพื่อให้ Roboto เป็นแบบอักษรเริ่มต้นทั่วทั้งแอป คุณสามารถสำรวจ fonts.google.com และใช้แบบอักษรที่คุณพบในส่วนต่างๆ ของแอป

คุณใช้วิดเจ็ตตัวช่วยจากไฟล์ lib/src/widgets.dart ในรูปแบบของ Header , Paragraph และ IconAndDetail วิดเจ็ตเหล่านี้จะกำจัดโค้ดที่ซ้ำกันเพื่อลดความยุ่งเหยิงในเค้าโครงหน้าที่อธิบายไว้ใน HomePage นอกจากนี้ยังช่วยให้มีรูปลักษณ์และความรู้สึกที่สอดคล้องกันอีกด้วย

หน้าตาแอปของคุณบน Android, iOS, เว็บ และ macOS มีดังนี้

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

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

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

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

3. สร้างและกำหนดค่าโปรเจ็กต์ Firebase

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

สร้างโปรเจ็กต์ Firebase

  1. ลงชื่อเข้าใช้ Firebase
  2. ในคอนโซล ให้คลิก เพิ่มโปรเจ็กต์ หรือ สร้างโปรเจ็กต์
  3. ในช่องชื่อ โปรเจ็กต์ ให้ป้อน Firebase-Flutter-Codelab แล้วคลิก ดำเนินการต่อ

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

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

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

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

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

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

58e3e3e23c2f16a4.png

เปิดใช้งาน Firestore

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

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

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

99e8429832d23fa3.png

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

6be00e26c72ea032.png

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

278656eefcfb0216.png

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

4. กำหนดค่า Firebase

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

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

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

กำหนดค่าการขึ้นต่อกัน

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

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

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

$ flutter pub add firebase_auth

แพ็คเกจ firebase_auth ช่วยให้สามารถทำงานร่วมกับ Authentication ได้

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

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

$ flutter pub add firebase_ui_auth

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

ติดตั้ง FlutterFire CLI

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

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

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

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

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

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

$ flutterfire configure

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

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

กำหนดค่า macOS

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

macos/Runner/DebugProfile.entitlements

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

macos/Runner/Release.entitlements

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

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

5. เพิ่มฟังก์ชันตอบรับคำเชิญ

เมื่อคุณเพิ่ม Firebase ลงในแอปแล้ว คุณสามารถสร้างปุ่ม RSVP ที่ลงทะเบียนบุคคลด้วย Authentication ได้ สำหรับ Android Native, iOS Native และ Web มีแพ็คเกจ 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 สามารถใช้สร้างส่วนหนึ่งของแผนผังใหม่ได้เมื่อสถานะของแอปเปลี่ยนแปลง วิดเจ็ต AuthFunc คือวิดเจ็ตเสริมที่คุณทดสอบ

ทดสอบขั้นตอนการรับรองความถูกต้อง

cdf2d25e436bd48d.png

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

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

4ed811a25b0cf816.png

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

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

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

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

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

7c20dc8424bb1d84.png

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

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

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

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.dart';

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});

  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> createState() => _GuestBookState();
}

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

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

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

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

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

  1. แก้ไขเนื้อหาของ HomePage เพื่อเพิ่มสองบรรทัดต่อไปนี้ที่ส่วนท้ายของรายการลูกของ ListView :
const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

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

ตัวอย่างแอป

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

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

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

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

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

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

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

lib/app_state.dart

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

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

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

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

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

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

  • ในไฟล์ lib/home_page.dart ให้ทำการเปลี่ยนแปลงต่อไปนี้กับวิดเจ็ต HomePage :

lib/home_page.dart

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

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

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

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

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

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

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

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

713870af0b3b63c.png

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

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

ประสานข้อความ

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

lib/app_state.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. ในส่วนการกำหนดค่าเริ่มต้นของ ApplicationState ให้เพิ่มบรรทัดต่อไปนี้เพื่อสมัครสมาชิกแบบสอบถามบนคอลเล็กชันเอกสารเมื่อผู้ใช้เข้าสู่ระบบและยกเลิกการสมัครเมื่อออกจากระบบ:

lib/app_state.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

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

  1. ในไฟล์ lib/guest_book.dart ให้เพิ่มการนำเข้าต่อไปนี้:
import 'guest_book_message.dart';
  1. ในวิดเจ็ต GuestBook ให้เพิ่มรายการข้อความเป็นส่วนหนึ่งของการกำหนดค่าเพื่อเชื่อมต่อสถานะการเปลี่ยนแปลงนี้กับอินเทอร์เฟซผู้ใช้:

lib/guest_book.dart

class GuestBook extends StatefulWidget {
  // Modify the following line:
  const GuestBook({
    super.key, 
    required this.addMessage, 
    required this.messages,
  });

  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}
  1. ใน _GuestBookState ให้แก้ไขวิธี build ดังต่อไปนี้เพื่อแสดงการกำหนดค่านี้:

lib/guest_book.dart

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

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

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

  1. อัปเดตเนื้อหาของ HomePage เพื่อสร้าง GuestBook อย่างถูกต้องด้วยพารามิเตอร์ messages ใหม่:

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

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

Firestore ซิงโครไนซ์ข้อมูลกับไคลเอนต์ที่สมัครเป็นสมาชิกฐานข้อมูลโดยอัตโนมัติและทันที

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

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

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

ตัวอย่างแอป

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

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

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

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

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

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

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

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

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

7767a2d2e64e7275.png

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

ใน match /databases/{database}/documents ให้ระบุคอลเลกชันที่คุณต้องการรักษาความปลอดภัย:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

เนื่องจากคุณใช้ UID การตรวจสอบสิทธิ์เป็นช่องในเอกสารสมุดเยี่ยมแต่ละฉบับ คุณสามารถรับ UID การตรวจสอบสิทธิ์และตรวจสอบว่าใครก็ตามที่พยายามเขียนลงในเอกสารนั้นมี UID การตรวจสอบสิทธิ์ที่ตรงกัน

  1. เพิ่มกฎการอ่านและเขียนให้กับชุดกฎของคุณ:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

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

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

9. ขั้นตอนพิเศษ: ฝึกฝนสิ่งที่คุณได้เรียนรู้มา

บันทึกสถานะการตอบรับคำเชิญของผู้เข้าร่วม

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

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

  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();
    });
  }

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

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

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