Firebase 跨裝置程式碼研究室

1. 簡介

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

適用於跨裝置通訊的 FlutterFire

我們發現許多家用裝置自動化、穿戴式裝置和個人健康科技裝置紛紛出現在網路上,因此在建構行動應用程式的過程中,跨裝置通訊變得越來越重要。與一般的行動應用程式相比,設定跨裝置通訊 (例如透過手機應用程式控制瀏覽器或控管電視播放內容) 通常較為複雜。

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

建構項目

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

  • 您在 Android、iOS 和網路上使用簡單好用的音樂播放器,並採用 Flutter 建構而成。
  • 允許使用者登入。
  • 在使用者於多部裝置上登入時連結裝置。
  • 允許使用者透過另一部裝置控制在某部裝置上的音樂播放。

7f0279938e1d3ab5.gif

課程內容

  • 如何建構並執行 Flutter 音樂播放器應用程式。
  • 如何允許使用者透過 Firebase Auth 登入。
  • 如何使用 Firebase RTDB Presence API 和 Firebase Installation Service 連結裝置。

軟硬體需求

  • 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 模擬器,請下載也支援 Flutter 開發的 Android Studio,並按照「建立及管理虛擬裝置」一文的說明操作。

您必須擁有 Mac 環境才能建立 iOS 模擬工具。下載 XCode,然後按照「Simulator Overview」(模擬工具總覽) 中的指示操作 >使用模擬器 >開啟並關閉模擬器

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. 執行 dart pub global activate flutterfire_cli 以安裝 FlutterFire CLI。
  4. 執行 flutterfire configure 來設定 FlutterFire CLI。
  5. 在提示中,選擇剛剛為本程式碼研究室建立的專案,例如 Firebase-Cross-Device-Codelab
  6. 當系統提示您選擇設定支援服務時,請選取「iOS」、「Android」和「網路」
  7. 系統提示您輸入 Apple bundle 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);您將使用 SizeS 追蹤裝置上線/離線狀態
  • Firebase 安全性規則可讓您確保資料庫安全無虞。
  • Firebase 安裝服務,用於識別屬於單一使用者的裝置。

4. 新增 Firebase 驗證

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

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

  1. 在 Firebase 控制台中,展開左側面板中的「建構」選單。
  2. 點選「驗證」,然後依序點選「開始使用」按鈕和「登入方式」分頁標籤。
  3. 按一下「登入供應商」清單中的「電子郵件/密碼」,將「啟用」的切換按鈕設為開啟,然後按一下「儲存」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 控制台的「即時資料庫」部分。按一下「建立資料庫」
  2. 如果系統提示您為安全性規則選取啟動模式,請暫時選取「Test Mode」。**(測試模式會建立安全性規則,允許所有要求通過,您稍後會新增安全性規則。在測試模式中,您的安全性規則一律不應進入正式環境)。

資料庫目前沒有任何內容。在「General」分頁下方的「Project settings」中找到您的 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 控制台,您應該會看到裝置出現在資料庫的一個使用者 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 控制台中,依序前往「即時資料庫」和「規則」分頁。請貼上下列規則,只允許已登入的使用者讀取及寫入自己的裝置狀態:

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

8. 恭喜!

bcd986f7106d892b.gif

恭喜,您已成功使用 Flutter 建構跨裝置遠端控制器!

抵免額

發揮 Firebase 的最大效益

  • 萊恩維農音樂
  • 歌詞和專輯封面 Marissa Christy
  • Voice by JP Gomez

9. 獎金

此外,建議您使用 Flutter FutureBuilder,將目前的待開發客戶裝置類型以非同步方式新增至 UI。如需相關協助,我們會將其導入包含程式碼研究室完成狀態的資料夾。

參考文件與後續步驟