Firebase 跨裝置程式碼研究室

1. 簡介

上次更新時間:2022 年 3 月 14 日

使用 FlutterFire 進行跨裝置通訊

隨著大量居家自動化、穿戴式和個人健康技術裝置上線,跨裝置通訊在建構行動應用程式時變得越來越重要。設定跨裝置通訊 (例如透過手機應用程式控制瀏覽器,或透過手機控制電視上播放的內容) 比建構一般行動應用程式複雜。

Firebase 的即時資料庫提供狀態 API ,可讓使用者查看裝置的連線/離線狀態;您將搭配使用 Firebase Installations Service,追蹤並連結同一位使用者登入的所有裝置。您將使用 Flutter 快速建立多個平台適用的應用程式,然後建構跨裝置原型,在一部裝置上播放音樂,並在另一部裝置上控制音樂!

建構項目

在本程式碼研究室中,您將建構簡單的音樂播放器遙控器。您的應用程式將會:

  • 在 Android、iOS 和網頁上使用 Flutter 建構簡易的音樂播放器。
  • 允許使用者登入。
  • 在多部裝置上登入同一位使用者時,連結這些裝置。
  • 允許使用者透過其他裝置控制某部裝置的音樂播放。

7f0279938e1d3ab5.gif

課程內容

  • 如何建構及執行 Flutter 音樂播放器應用程式。
  • 如何允許使用者透過 Firebase 驗證登入。
  • 瞭解如何使用 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> 指令安裝。

網頁應用程式應會顯示基本的獨立音樂播放器。確認播放器功能運作正常。這是專為本程式碼研究室設計的簡單音樂播放器應用程式。只能播放 Firebase 歌曲「Better Together」

設定 Android 模擬器或 iOS 模擬器

如果您已有用於開發的 Android 裝置或 iOS 裝置,可以略過這個步驟。

如要建立 Android 模擬器,請下載 Android Studio (也支援 Flutter 開發),然後按照「建立及管理虛擬裝置」一文中的操作說明進行。

如要建立 iOS 模擬器,您需要 Mac 環境。下載 XCode,然後按照「模擬器總覽」>「使用模擬器」>「開啟及關閉模擬器」中的指示操作。

3. 設定 Firebase

建立 Firebase 專案

  1. 使用 Google 帳戶登入 Firebase 控制台
  2. 按一下按鈕建立新專案,然後輸入專案名稱 (例如 Firebase-Cross-Device-Codelab)。
  3. 按一下「繼續」
  4. 如果系統提示,請詳閱並接受 Firebase 條款,然後按一下「繼續」
  5. (選用) 在 Firebase 控制台中啟用 AI 輔助功能 (稱為「Gemini in Firebase」)。
  6. 本程式碼研究室不需要 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. 執行 dart pub global activate flutterfire_cli 安裝 FlutterFire CLI。
  4. 執行 flutterfire configure 設定 FlutterFire CLI。
  5. 在提示中,選擇您剛為本程式碼研究室建立的專案,例如「Firebase-Cross-Device-Codelab」
  6. 系統提示選擇設定支援時,請選取「iOS」、「Android」和「網頁」
  7. 系統提示輸入 Apple 組合 ID 時,請輸入不重複的網域,或輸入 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 Installations Service,用於識別單一使用者登入的裝置。

4. 新增 Firebase Auth

為 Firebase 驗證啟用電子郵件登入

如要允許使用者登入網頁應用程式,請使用「電子郵件/密碼」登入方式:

  1. 在 Firebase 控制台中,展開左側面板的「Build」選單。
  2. 依序點選「Authentication」和「Get Started」按鈕,然後點選「Sign-in method」分頁標籤。
  3. 在「Sign-in providers」清單中點選「Email/Password」,將「Enable」切換到開啟位置,然後點選「Save」58e3e3e23c2f16a4.png

在 Flutter 中設定 Firebase 驗證

在指令列中執行下列指令,安裝必要的 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,
          ),
        ),
      ]),
    );
  }
}

第三,在 main.dart. 中找出現有的 appBar 小工具,然後新增 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 控制台的「驗證」下方,您應該會看到電子郵件地址列為新使用者。

888506c86a28a72c.png

恭喜!使用者現在可以登入應用程式了!

5. 新增資料庫連線

現在您可以使用 Firebase Presence API 註冊裝置。

在指令列中執行下列指令,加入必要的依附元件:

flutter pub add firebase_app_installations

flutter pub add firebase_database

建立資料庫

在 Firebase 控制台中,

  1. 前往 Firebase 控制台的「Realtime Database」部分。按一下「建立資料庫」
  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 Installations 和 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 控制台中,您應該會在資料庫中看到裝置顯示在一個使用者 ID 底下。

5bef49cea3564248.png

6. 同步處理裝置狀態

選取主要裝置

如要在裝置之間同步狀態,請將其中一個裝置指定為領導者或控制器。主裝置會決定追蹤裝置的狀態。

application_state.dart 中建立 setLeadDevice 方法,並在 RTDB 中使用 active_device 鍵追蹤這部裝置:

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

如要在應用程式列內容選單中新增這項功能,請修改 SignedInMenuButton 小工具,建立名為 ControllerPopupMenuItem。使用者可透過這個選單設定主要裝置。

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

從資料庫讀取主要裝置的狀態

如要讀取及使用主裝置的狀態,需要瞭解兩部分內容。首先,您要在 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 控制台中,前往 Realtime Database,然後前往「規則」分頁標籤。貼上下列規則,只允許登入的使用者讀取及寫入自己的裝置狀態:

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

8. 恭喜!

bcd986f7106d892b.gif

恭喜!您已使用 Flutter 成功建構跨裝置遙控器!

抵免額

Better Together (Firebase 之歌)

  • 萊恩佛農的音樂
  • 歌詞和專輯封面由 Marissa Christy 提供
  • JP Gomez 配音

9. 獎金

不妨使用 Flutter FutureBuilder,以非同步方式將目前的領先裝置類型新增至 UI,增加挑戰性。如需輔助功能,請在包含程式碼研究室完成狀態的資料夾中實作。

參考文件和後續步驟