Codelab ของ Firebase ข้ามอุปกรณ์

1. บทนำ

อัปเดตล่าสุด: 2022-03-14

FlutterFire สำหรับการสื่อสารข้ามอุปกรณ์

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

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

สิ่งที่คุณจะสร้าง

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

  • มีเครื่องเล่นเพลงที่เรียบง่ายบน Android, iOS และเว็บ ซึ่งสร้างด้วย Flutter
  • อนุญาตให้ผู้ใช้ลงชื่อเข้าใช้
  • เชื่อมต่ออุปกรณ์เมื่อผู้ใช้รายเดียวกันลงชื่อเข้าใช้ในอุปกรณ์หลายเครื่อง
  • อนุญาตให้ผู้ใช้ควบคุมการเล่นเพลงในอุปกรณ์เครื่องหนึ่งจากอุปกรณ์อีกเครื่องหนึ่ง

7f0279938e1d3ab5.gif

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

  • วิธีสร้างและเรียกใช้แอปมิวสิกเพลเยอร์ Flutter
  • วิธีอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้ด้วย Firebase Auth
  • วิธีใช้ Firebase RTDB Presence API และบริการติดตั้งใช้งาน Firebase เพื่อเชื่อมต่ออุปกรณ์

สิ่งที่คุณต้องมี

  • สภาพแวดล้อมในการพัฒนา Flutter ทำตามวิธีการในคู่มือการติดตั้ง Flutter เพื่อตั้งค่า
  • ต้องใช้ Flutter เวอร์ชัน 2.10 ขึ้นไป หากใช้เวอร์ชันต่ำกว่า ให้เรียกใช้ flutter upgrade.
  • บัญชี Firebase

2. การเริ่มตั้งค่า

รับโค้ดเริ่มต้น

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

git clone https://github.com/FirebaseExtended/cross-device-controller.git

cd cross-device-controller/starter_code

flutter pub get

สร้างแอป

คุณสามารถใช้ IDE ที่ชื่นชอบเพื่อสร้างแอป หรือจะใช้บรรทัดคำสั่งก็ได้

ในไดเรกทอรีแอป ให้สร้างแอปสำหรับเว็บด้วยคำสั่ง flutter run -d web-server.คุณควรเห็นข้อความแจ้งต่อไปนี้

lib/main.dart is being served at http://localhost:<port>

ไปที่ http://localhost:<port> เพื่อดูมิวสิกเพลเยอร์

หากคุ้นเคยกับโปรแกรมจำลอง Android หรือโปรแกรมจำลอง iOS คุณสามารถสร้างแอปสำหรับแพลตฟอร์มเหล่านั้นและติดตั้งด้วยคำสั่ง flutter run -d <device_name>

เว็บแอปควรแสดงมิวสิกเพลเยอร์แบบสแตนด์อโลนพื้นฐาน ตรวจสอบว่าฟีเจอร์ของเพลเยอร์ทำงานได้ตามที่ต้องการ นี่คือแอปเครื่องเล่นเพลงแบบง่ายที่ออกแบบมาสำหรับ Codelab นี้ โดยจะเล่นได้เฉพาะเพลง Better Together ของ Firebase เท่านั้น

ตั้งค่าโปรแกรมจำลอง Android หรือเครื่องมือจำลอง iOS

หากมีอุปกรณ์ Android หรืออุปกรณ์ iOS สำหรับการพัฒนาอยู่แล้ว ให้ข้ามขั้นตอนนี้

หากต้องการสร้างโปรแกรมจำลอง Android ให้ดาวน์โหลด Android Studio ซึ่งรองรับการพัฒนา Flutter ด้วย แล้วทำตามวิธีการในสร้างและจัดการอุปกรณ์เสมือน

หากต้องการสร้างโปรแกรมจำลอง iOS คุณจะต้องมีสภาพแวดล้อม Mac ดาวน์โหลด XCode แล้วทำตามวิธีการในภาพรวมของโปรแกรมจำลอง > ใช้โปรแกรมจำลอง > เปิดและปิดโปรแกรมจำลอง

3. ตั้งค่า Firebase

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

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

ติดตั้ง Firebase SDK

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

flutter pub add firebase_core

ในไฟล์ pubspec.yaml ให้แก้ไขเวอร์ชันสำหรับ firebase_core เป็น 1.13.1 ขึ้นไป หรือเรียกใช้ flutter upgrade

เริ่มต้น FlutterFire

  1. หากยังไม่ได้ติดตั้ง Firebase CLI คุณสามารถติดตั้งได้โดยเรียกใช้ curl -sL https://firebase.tools | bash
  2. เข้าสู่ระบบโดยเรียกใช้ firebase login แล้วทำตามข้อความแจ้ง
  3. ติดตั้ง FlutterFire CLI โดยเรียกใช้ dart pub global activate flutterfire_cli
  4. กำหนดค่า FlutterFire CLI โดยเรียกใช้ flutterfire configure
  5. เมื่อได้รับข้อความแจ้ง ให้เลือกโปรเจ็กต์ที่คุณเพิ่งสร้างสำหรับโค้ดแล็บนี้ เช่น Firebase-Cross-Device-Codelab
  6. เลือก iOS, Android และ Web เมื่อระบบแจ้งให้เลือกการรองรับการกำหนดค่า
  7. เมื่อได้รับแจ้งให้ป้อนรหัสแพ็กเกจ Apple ให้พิมพ์โดเมนที่ไม่ซ้ำกัน หรือป้อน com.example.appname ซึ่งใช้ได้ตามวัตถุประสงค์ของโค้ดแล็บนี้

เมื่อกำหนดค่าแล้ว ระบบจะสร้างไฟล์ firebase_options.dart ให้คุณซึ่งมีตัวเลือกทั้งหมดที่จำเป็นสำหรับการเริ่มต้น

ในโปรแกรมแก้ไข ให้เพิ่มโค้ดต่อไปนี้ลงในไฟล์ main.dart เพื่อเริ่มต้น Flutter และ Firebase

lib/main.dart

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
 
void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp(
   options: DefaultFirebaseOptions.currentPlatform,
 );
 runApp(const MyMusicBoxApp());
}

คอมไพล์แอปด้วยคำสั่งต่อไปนี้

flutter run

คุณยังไม่ได้เปลี่ยนองค์ประกอบ UI ใดๆ ลักษณะและลักษณะการทำงานของแอปจึงยังไม่เปลี่ยนแปลง แต่ตอนนี้คุณมีแอป Firebase แล้ว และเริ่มใช้ผลิตภัณฑ์ Firebase ได้ ซึ่งรวมถึง

  • การตรวจสอบสิทธิ์ Firebase ซึ่งช่วยให้ผู้ใช้ลงชื่อเข้าใช้แอปได้
  • ฐานข้อมูลเรียลไทม์ของ Firebase(RTDB): คุณจะใช้ Presence API เพื่อติดตามสถานะออนไลน์/ออฟไลน์ของอุปกรณ์
  • กฎการรักษาความปลอดภัยของ Firebase จะช่วยให้คุณรักษาความปลอดภัยของฐานข้อมูลได้
  • บริการการติดตั้ง Firebase เพื่อระบุอุปกรณ์ที่ผู้ใช้รายเดียวลงชื่อเข้าใช้

4. เพิ่ม Firebase Auth

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

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

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

กำหนดค่าการตรวจสอบสิทธิ์ Firebase ใน Flutter

ในบรรทัดคำสั่ง ให้เรียกใช้คำสั่งต่อไปนี้เพื่อติดตั้งแพ็กเกจ Flutter ที่จำเป็น

flutter pub add firebase_auth

flutter pub add provider

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

วางโค้ดต่อไปนี้ลงในไฟล์ application_state.dart ใหม่

lib/src/application_state.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';

import '../firebase_options.dart';
import 'authentication.dart';

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

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

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

หากต้องการให้ระบบเริ่มต้น ApplicationState เมื่อแอปเริ่มทำงาน คุณจะต้องเพิ่มขั้นตอนการเริ่มต้นไปยัง main.dart ดังนี้

lib/main.dart

import 'src/application_state.dart'; 
import 'package:provider/provider.dart';

void main() async {
  ... 
  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: (context, _) => const MyMusicBoxApp(),
  ));
}

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

สร้างโฟลว์การลงชื่อเข้าใช้

ในขั้นตอนนี้ คุณจะทำงานกับขั้นตอนการลงชื่อเข้าใช้และลงชื่อออก ขั้นตอนการทำงานจะเป็นดังนี้

  1. ผู้ใช้ที่ออกจากระบบจะเริ่มขั้นตอนการลงชื่อเข้าใช้โดยคลิกเมนูบริบท 71fcc1030a336423.png ทางด้านขวาของแถบแอป
  2. ขั้นตอนการลงชื่อเข้าใช้จะแสดงในกล่องโต้ตอบ
  3. หากผู้ใช้ไม่เคยลงชื่อเข้าใช้มาก่อน ระบบจะแจ้งให้สร้างบัญชีโดยใช้อีเมลและรหัสผ่านที่ถูกต้อง
  4. หากผู้ใช้เคยลงชื่อเข้าใช้มาก่อน ระบบจะแจ้งให้ผู้ใช้ป้อนรหัสผ่าน
  5. เมื่อผู้ใช้ลงชื่อเข้าใช้แล้ว การคลิกเมนูตามบริบทจะแสดงตัวเลือกออกจากระบบ

c295f6fa2e1d40f3.png

การเพิ่มขั้นตอนการลงชื่อเข้าใช้ต้องมี 3 ขั้นตอน

ก่อนอื่น ให้สร้างAppBarMenuButtonวิดเจ็ต วิดเจ็ตนี้จะควบคุมป๊อปอัปเมนูตามบริบทโดยขึ้นอยู่กับ loginState ของผู้ใช้ เพิ่มการนำเข้า

lib/src/widgets.dart

import 'application_state.dart';
import 'package:provider/provider.dart';
import 'authentication.dart';

ต่อท้ายโค้ดต่อไปนี้ใน widgets.dart.

lib/src/widgets.dart

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

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
      builder: (context, appState, child) {
        if (appState.loginState == ApplicationLoginState.loggedIn) {
          return SignedInMenuButton(buildContext: context);
        }
        return SignInMenuButton(buildContext: context);
      },
    );
  }
}

class SignedInMenuButton extends StatelessWidget {
  const SignedInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      onSelected: _handleSignedInMenu,
      color: Colors.deepPurple.shade300,
      itemBuilder: (context) => _getMenuItemBuilder(),
    );
  }

  List<PopupMenuEntry<String>> _getMenuItemBuilder() {
    return [
      const PopupMenuItem<String>(
        value: 'Sign out',
        child: Text(
          'Sign out',
          style: TextStyle(color: Colors.white),
        ),
      )
    ];
  }

  Future<void> _handleSignedInMenu(String value) async {
    switch (value) {
      case 'Sign out':
        Provider.of<ApplicationState>(buildContext, listen: false).signOut();
        break;
    }
  }
}

class SignInMenuButton extends StatelessWidget {
  const SignInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      onSelected: _signIn,
      color: Colors.deepPurple.shade300,
      itemBuilder: (context) => _getMenuItemBuilder(context),
    );
  }

  Future<void> _signIn(String value) async {
    return showDialog<void>(
      context: buildContext,
      builder: (context) => const SignInDialog(),
    );
  }

  List<PopupMenuEntry<String>> _getMenuItemBuilder(BuildContext context) {
    return [
      const PopupMenuItem<String>(
        value: 'Sign in',
        child: Text(
          'Sign in',
          style: TextStyle(color: Colors.white),
        ),
      ),
    ];
  }
}

ประการที่ 2 ให้สร้างSignInDialogวิดเจ็ตในwidgets.dartชั้นเรียนเดียวกัน

lib/src/widgets.dart

class SignInDialog extends AlertDialog {
  const SignInDialog({Key? key}) : super(key: key);

  @override
  AlertDialog build(BuildContext context) {
    return AlertDialog(
      content: Column(mainAxisSize: MainAxisSize.min, children: [
        Consumer<ApplicationState>(
          builder: (context, appState, _) => Authentication(
            email: appState.email,
            loginState: appState.loginState,
            startLoginFlow: appState.startLoginFlow,
            verifyEmail: appState.verifyEmail,
            signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
            cancelRegistration: appState.cancelRegistration,
            registerAccount: appState.registerAccount,
            signOut: appState.signOut,
          ),
        ),
      ]),
    );
  }
}

ประการที่สาม ค้นหาวิดเจ็ต appBar ที่มีอยู่แล้วใน main.dart. Add the AppBarMenuButton to display the Sign in or Sign out option

lib/main.dart

import 'src/widgets.dart';
appBar: AppBar(
  title: const Text('Music Box'),
  backgroundColor: Colors.deepPurple.shade400,
  actions: const <Widget>[
    AppBarMenuButton(),
  ],
),

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

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

ในคอนโซล Firebase คุณควรเห็นอีเมลที่แสดงเป็นผู้ใช้ใหม่ในส่วนการตรวจสอบสิทธิ์

888506c86a28a72c.png

ยินดีด้วย ตอนนี้ผู้ใช้ลงชื่อเข้าใช้แอปได้แล้ว

5. เพิ่มการเชื่อมต่อฐานข้อมูล

ตอนนี้คุณพร้อมที่จะไปยังการลงทะเบียนอุปกรณ์โดยใช้ Firebase Presence API แล้ว

ในบรรทัดคำสั่ง ให้เรียกใช้คำสั่งต่อไปนี้เพื่อเพิ่มทรัพยากร Dependency ที่จำเป็น

flutter pub add firebase_app_installations

flutter pub add firebase_database

สร้างฐานข้อมูล

ในคอนโซล Firebase

  1. ไปที่ส่วนฐานข้อมูลเรียลไทม์ของคอนโซล Firebase คลิกสร้างฐานข้อมูล
  2. หากระบบแจ้งให้เลือกโหมดเริ่มต้นสำหรับกฎความปลอดภัย ให้เลือกโหมดทดสอบในตอนนี้** (โหมดทดสอบจะสร้างกฎความปลอดภัยที่อนุญาตคำขอทั้งหมด คุณจะเพิ่มกฎความปลอดภัยในภายหลัง คุณไม่ควรใช้กฎความปลอดภัยในโหมดทดสอบในสภาพแวดล้อมการใช้งานจริง)

ขณะนี้ฐานข้อมูลว่างเปล่า ค้นหา databaseURL ในการตั้งค่าโปรเจ็กต์ในแท็บทั่วไป เลื่อนลงไปที่ส่วนเว็บแอป

1b6076f60a36263b.png

เพิ่ม databaseURL ลงในไฟล์ firebase_options.dart:

lib/firebase_options.dart

 static const FirebaseOptions web = FirebaseOptions(
    apiKey: yourApiKey,
    ...
    databaseURL: 'https://<YOUR_DATABASE_URL>,
    ...
  );

ลงทะเบียนอุปกรณ์โดยใช้ Presence API ของ RTDB

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

lib/src/application_state.dart

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart'; 

class ApplicationState extends ChangeNotifier {

  String? _deviceId;
  String? _uid;

  Future<void> init() async {
    ...
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _uid = user.uid;
        _addUserDevice();
      }
      ...
    });
  }

  Future<void> _addUserDevice() async {
    _uid = FirebaseAuth.instance.currentUser?.uid;

    String deviceType = _getDevicePlatform();
    // Create two objects which we will write to the
    // Realtime database when this device is offline or online
    var isOfflineForDatabase = {
      'type': deviceType,
      'state': 'offline',
      'last_changed': ServerValue.timestamp,
    };
    var isOnlineForDatabase = {
      'type': deviceType,
      'state': 'online',
      'last_changed': ServerValue.timestamp,
    };

    var devicesRef =
        FirebaseDatabase.instance.ref().child('/users/$_uid/devices');

    FirebaseInstallations.instance
        .getId()
        .then((id) => _deviceId = id)
        .then((_) {
      // Use the semi-persistent Firebase Installation Id to key devices
      var deviceStatusRef = devicesRef.child('$_deviceId');

      // RTDB Presence API
      FirebaseDatabase.instance
          .ref()
          .child('.info/connected')
          .onValue
          .listen((data) {
        if (data.snapshot.value == false) {
          return;
        }

        deviceStatusRef.onDisconnect().set(isOfflineForDatabase).then((_) {
          deviceStatusRef.set(isOnlineForDatabase);
        });
      });
    });
  }

  String _getDevicePlatform() {
    if (kIsWeb) {
      return 'Web';
    } else if (Platform.isIOS) {
      return 'iOS';
    } else if (Platform.isAndroid) {
      return 'Android';
    }
    return 'Unknown';
  }

กลับไปที่บรรทัดคำสั่ง สร้างและเรียกใช้แอปบนอุปกรณ์หรือในเบราว์เซอร์ด้วย flutter run.

ลงชื่อเข้าใช้เป็นผู้ใช้ในแอป อย่าลืมลงชื่อเข้าใช้ในแพลตฟอร์มต่างๆ ด้วยผู้ใช้รายเดียวกัน

ในคอนโซล Firebase คุณควรเห็นอุปกรณ์ปรากฏภายใต้รหัสผู้ใช้เดียวในฐานข้อมูล

5bef49cea3564248.png

6. ซิงค์สถานะอุปกรณ์

เลือกอุปกรณ์หลัก

หากต้องการซิงค์สถานะระหว่างอุปกรณ์ ให้กำหนดอุปกรณ์เครื่องหนึ่งเป็นผู้นำหรือตัวควบคุม อุปกรณ์หลักจะเป็นตัวกำหนดสถานะในอุปกรณ์รอง

สร้างsetLeadDeviceใน application_state.dart และติดตามอุปกรณ์นี้ด้วยคีย์ active_device ใน RTDB โดยทำดังนี้

lib/src/application_state.dart

  bool _isLeadDevice = false;
  String? leadDeviceType;

  Future<void> setLeadDevice() async {
    if (_uid != null && _deviceId != null) {
      var playerRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      await playerRef
          .update({'id': _deviceId, 'type': _getDevicePlatform()}).then((_) {
        _isLeadDevice = true;
      });
    }
  }

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

lib/src/widgets.dart

class SignedInMenuButton extends StatelessWidget {
  const SignedInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  List<PopupMenuEntry<String>> _getMenuItemBuilder() {
    return [
      const PopupMenuItem<String>(
        value: 'Sign out',
        child: Text(
          'Sign out',
          style: TextStyle(color: Colors.white),
        ),
      ),
      const PopupMenuItem<String>(
        value: 'Controller',
        child: Text(
          'Set as controller',
          style: TextStyle(color: Colors.white),
        ),
      )
    ];
  }

  void _handleSignedInMenu(String value) async {
    switch (value) {
      ...
      case 'Controller':
        Provider.of<ApplicationState>(buildContext, listen: false)
            .setLeadDevice();
    }
  }
}

เขียนสถานะของอุปกรณ์หลักลงในฐานข้อมูล

เมื่อตั้งค่าอุปกรณ์หลักแล้ว คุณจะซิงค์สถานะของอุปกรณ์หลักกับ RTDB ได้ด้วยโค้ดต่อไปนี้ ต่อท้ายโค้ดต่อไปนี้ที่ส่วนท้ายของ application_state.dart.This will start storing two attributes: the player state (play or pause) and the slider position.

lib/src/application_state.dart

  Future<void> setLeadDeviceState(
      int playerState, double sliderPosition) async {
    if (_isLeadDevice && _uid != null && _deviceId != null) {
      var leadDeviceStateRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      try {
        var playerSnapshot = {
          'id': _deviceId,
          'state': playerState,
          'type': _getDevicePlatform(),
          'slider_position': sliderPosition
        };
        await leadDeviceStateRef.set(playerSnapshot);
      } catch (e) {
        throw Exception('updated playerState with error');
      }
    }
  }

และสุดท้าย คุณต้องเรียกใช้ setActiveDeviceState ทุกครั้งที่สถานะเพลเยอร์ของตัวควบคุมมีการอัปเดต ทำการเปลี่ยนแปลงต่อไปนี้กับไฟล์ player_widget.dart ที่มีอยู่

lib/player_widget.dart

import 'package:provider/provider.dart';
import 'application_state.dart';

 void _onSliderChangeHandler(v) {
    ...
    // update player state in RTDB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(_playerState.index, _sliderPosition);
 }

 Future<int> _pause() async {
    ...
    // update DB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(_playerState.index, _sliderPosition);
    return result;
  }

 Future<int> _play() async {
    var result = 0;

    // update DB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(PlayerState.PLAYING.index, _sliderPosition);

    if (_playerState == PlayerState.PAUSED) {
      result = await _audioPlayer.resume();
      return result;
    }
    ...
 }

 Future<int> _updatePositionAndSlider(Duration tempPosition) async {
    ...
    // update DB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(_playerState.index, _sliderPosition);
    return result;
  }

อ่านสถานะของอุปกรณ์หลักจากฐานข้อมูล

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

lib/src/application_state.dart

// Interface to be implemented by PlayerWidget
typedef OnLeadDeviceChangeCallback = void Function(
    Map<dynamic, dynamic> snapshot);

class ApplicationState extends ChangeNotifier {
  ...

  OnLeadDeviceChangeCallback? onLeadDeviceChangeCallback;

  Future<void> init() async {
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _uid = user.uid;
        _addUserDevice().then((_) => listenToLeadDeviceChange());
      }
      ...
    });
  }

  Future<void> listenToLeadDeviceChange() async {
    if (_uid != null) {
      var activeDeviceRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      activeDeviceRef.onValue.listen((event) {
        final activeDeviceState = event.snapshot.value as Map<dynamic, dynamic>;
        String activeDeviceKey = activeDeviceState['id'] as String;
        _isLeadDevice = _deviceId == activeDeviceKey;
        leadDeviceType = activeDeviceState['type'] as String;
        if (!_isLeadDevice) {
          onLeadDeviceChangeCallback?.call(activeDeviceState);
        }
        notifyListeners();
      });
    }
  }

ประการที่ 2 ให้เริ่มโปรแกรมฟังฐานข้อมูลระหว่างการเริ่มต้นเพลเยอร์ใน player_widget.dart ส่งฟังก์ชัน _updatePlayer เพื่อให้ระบบอัปเดตสถานะเพลเยอร์ที่ติดตามได้ทุกครั้งที่ค่าในฐานข้อมูลเปลี่ยนแปลง

lib/player_widget.dart

class _PlayerWidgetState extends State<PlayerWidget> {

  @override
  void initState() {
    ...
    Provider.of<ApplicationState>(context, listen: false)
        .onLeadDeviceChangeCallback = updatePlayer;
  }

  void updatePlayer(Map<dynamic, dynamic> snapshot) {
    _updatePlayer(snapshot['state'], snapshot['slider_position']);
  }

  void _updatePlayer(dynamic state, dynamic sliderPosition) {
    if (state is int && sliderPosition is double) {
      try {
        _updateSlider(sliderPosition);
        final PlayerState newState = PlayerState.values[state];
        if (newState != _playerState) {
          switch (newState) {
            case PlayerState.PLAYING:
              _play();
              break;
            case PlayerState.PAUSED:
              _pause();
              break;
            case PlayerState.STOPPED:
            case PlayerState.COMPLETED:
              _stop();
              break;
          }
          _playerState = newState;
        }
      } catch (e) {
        if (kDebugMode) {
          print('sync player failed');
        }
      }
    }
  }

ตอนนี้คุณพร้อมที่จะทดสอบแอปแล้ว โดยทำดังนี้

  1. ในบรรทัดคำสั่ง ให้เรียกใช้แอปในโปรแกรมจำลองและ/หรือในเบราว์เซอร์ด้วย flutter run -d <device-name>
  2. เปิดแอปในเบราว์เซอร์, ในโปรแกรมจำลอง iOS หรือโปรแกรมจำลอง Android ไปที่เมนูบริบท แล้วเลือกแอปใดแอปหนึ่งให้เป็นอุปกรณ์หลัก คุณควรจะเห็นว่าเพลเยอร์ของอุปกรณ์ที่ติดตามจะเปลี่ยนไปเมื่ออุปกรณ์หลักอัปเดต
  3. ตอนนี้ให้เปลี่ยนอุปกรณ์หลัก เล่นหรือหยุดเพลงชั่วคราว แล้วสังเกตว่าอุปกรณ์รองจะอัปเดตตาม

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

7. อัปเดตกฎความปลอดภัย

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

{
  "rules": {
    "users": {
           "$uid": {
               ".read": "$uid === auth.uid",
               ".write": "$uid === auth.uid"
           }
    },
  }
}

8. ยินดีด้วย

bcd986f7106d892b.gif

ยินดีด้วย คุณสร้างรีโมตคอนโทรลข้ามอุปกรณ์โดยใช้ Flutter เรียบร้อยแล้ว

เครดิต

Better Together เพลงของ Firebase

  • Music by Ryan Vernon
  • เนื้อเพลงและปกอัลบั้มโดย Marissa Christy
  • เสียงโดย JP Gomez

9. โบนัส

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

เอกสารอ้างอิงและขั้นตอนถัดไป