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

1. บทนำ

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

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

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

ฐานข้อมูลเรียลไทม์ของ Firebase มี Presence 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 รหัสเริ่มต้นอยู่ใน repo Git ในการเริ่มต้น บนบรรทัดคำสั่ง ให้โคลน repo ย้ายไปยังโฟลเดอร์ที่มีสถานะเริ่มต้น และติดตั้งการอ้างอิง:

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 นี้ สามารถเล่นได้เฉพาะเพลง Firebase Better Together

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

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

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

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

3. การตั้งค่า Firebase

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

เปิดเบราว์เซอร์ไปที่ http://console.firebase.google.com/

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

คุณไม่จำเป็นต้องดาวน์โหลดไฟล์ที่กล่าวถึงหรือเปลี่ยนแปลงไฟล์ build.gradle คุณจะกำหนดค่าเหล่านี้เมื่อคุณเริ่มต้น FlutterFire

ติดตั้ง 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. เมื่อได้รับข้อความแจ้ง ให้เลือกโปรเจ็กต์ที่คุณเพิ่งสร้างสำหรับ Codelab นี้ เช่น Firebase-Cross-Device-Codelab
  6. เลือก iOS , Android และ เว็บ เมื่อคุณได้รับพร้อมท์ให้เลือกการสนับสนุนการกำหนดค่า
  7. เมื่อได้รับแจ้งให้ระบุ Apple Bundle ID ให้พิมพ์โดเมนที่ไม่ซ้ำกันหรือป้อน com.example.appname ซึ่งเหมาะสำหรับวัตถุประสงค์ของ Codelab นี้

เมื่อกำหนดค่าแล้ว ไฟล์ 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 Authentication ซึ่งอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้แอปของคุณได้
  • ฐานข้อมูลเรียลไทม์ Firebase (RTDB) ; คุณจะใช้ Presence API เพื่อติดตามสถานะออนไลน์/ออฟไลน์ของอุปกรณ์
  • กฎความปลอดภัยของ Firebase จะช่วยให้คุณรักษาความปลอดภัยฐานข้อมูล
  • บริการติดตั้ง Firebase เพื่อระบุอุปกรณ์ที่ผู้ใช้รายเดียวลงชื่อเข้าใช้

4. เพิ่มการตรวจสอบสิทธิ์ Firebase

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

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

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

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

ก่อนอื่น สร้างวิดเจ็ต 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),
        ),
      ),
    ];
  }
}

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

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. เพิ่ม AppBarMenuButton เพื่อแสดงตัวเลือก ลงชื่อเข้าใช้ หรือ ออกจากระบบ

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 ภายใต้ Authentication คุณควรจะเห็นที่อยู่อีเมลที่แสดงเป็นผู้ใช้ใหม่

888506c86a28a72c.png

ยินดีด้วย! ผู้ใช้สามารถลงชื่อเข้าใช้แอปได้แล้ว!

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

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

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

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>,
    ...
  );

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

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

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

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

มีสองส่วนในการอ่านและใช้สถานะของอุปกรณ์ตะกั่ว ขั้นแรก คุณต้องการตั้งค่า Listener ฐานข้อมูลของสถานะ Lead Player ใน application_state ผู้ฟังนี้จะบอกอุปกรณ์ผู้ติดตามเมื่อต้องอัปเดตหน้าจอผ่านการโทรกลับ โปรดสังเกตว่าคุณได้กำหนดอินเทอร์เฟ 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();
      });
    }
  }

ประการที่สอง เริ่มต้นการฟังฐานข้อมูลระหว่างการเริ่มต้นผู้เล่นใน 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. อัปเดตกฎความปลอดภัย

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

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

8. ขอแสดงความยินดี!

bcd986f7106d892b.gif

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

เครดิต

Better Together เพลงจาก Firebase

  • ดนตรีโดยไรอัน เวอร์นอน
  • เนื้อเพลงและปกอัลบั้มโดย Marissa Christy
  • พากย์เสียงโดย เจพี โกเมซ

9. โบนัส

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

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