Firebase クロスデバイス Codelab

1. はじめに

最終更新日: 2022 年 3 月 14 日

クロスデバイス通信用の FlutterFire

スマートホーム、ウェアラブル、パーソナル ヘルス テクノロジーのデバイスがオンラインに登場し、モバイル アプリケーションの開発において、デバイス間の通信がますます重要になっています。従来、スマートフォン アプリからブラウザを操作したり、テレビで再生するコンテンツをスマートフォンから制御したりするなど、クロスデバイス通信のセットアップは、通常のモバイルアプリを作成するよりも複雑でした。

Firebase の Realtime Database には、ユーザーがデバイスのオンライン / オフラインのステータスを確認できる Presence API が用意されています。この API を Firebase Installations Service とともに使用すると、同じユーザーがログインしているすべてのデバイスを追跡して接続できます。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 Emulator や iOS シミュレータに慣れている場合は、これらのプラットフォーム用のアプリを作成し、flutter run -d <device_name> コマンドを使用してインストールできます。

ウェブアプリに、基本的なスタンドアロンの音楽プレーヤーが表示されます。プレーヤー機能が意図したとおりに動作していることを確認します。これは、この Codelab 用に設計されたシンプルな音楽プレーヤー アプリです。再生できるのは Firebase の楽曲「Better Together」のみです。

Android Emulator または iOS シミュレータをセットアップする

開発用の Android デバイスまたは iOS デバイスがすでにある場合は、この手順をスキップできます。

Android エミュレータを作成するには、Flutter 開発もサポートする Android Studio をダウンロードし、仮想デバイスの作成と管理の手順に沿って操作します。

iOS シミュレータを作成するには、Mac 環境が必要です。XCode をダウンロードし、シミュレータの概要の手順に沿って操作する >シミュレーターを使用 >シミュレータを起動または終了します

3. Firebase の設定

Firebase プロジェクトを作成する

ブラウザを開き、http://console.firebase.google.com/ にアクセスします。

  1. Firebase にログインします。
  2. Firebase コンソールで [プロジェクトを追加](または [プロジェクトを作成])をクリックし、Firebase プロジェクトに Firebase-Cross-Device-Codelab という名前を付けます。
  3. プロジェクト作成オプションをクリックします。プロンプトが表示されたら、Firebase の利用規約に同意します。このアプリではアナリティクスを使用しないため、Google アナリティクスの設定はスキップします。

上記のファイルをダウンロードしたり、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. プロンプトが表示されたら、この Codelab 用に作成したプロジェクト(Firebase-Cross-Device-Codelab など)を選択します。
  6. 構成サポートを選択するよう求められたら、[iOS]、[Android]、[ウェブ] を選択します。
  7. [Apple バンドル 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 Realtime Database(RTDB): presence API を使用して、デバイスのオンライン / オフライン ステータスをトラッキングします。
  • Firebase セキュリティ ルールを使用してデータベースを保護できます。
  • Firebase インストール サービス: 1 人のユーザーがログインしているデバイスを特定します。

4. Firebase Auth を追加する

Firebase Authentication でメールによるログインを有効にする

ユーザーがウェブアプリにログインできるようにするには、メール / パスワードのログイン方法を使用します。

  1. Firebase コンソールの左側のパネルで [Build] メニューを開きます。
  2. [Authentication] をクリックし、[Get Started] ボタン、[Sign-in method] タブをクリックします。
  3. [ログイン プロバイダ] リストで [メール/パスワード] をクリックし、[有効にする] スイッチをオンにして、[保存] をクリックします。58e3e3e23c2f16a4.png

Flutter で Firebase Authentication を構成する

コマンドラインで次のコマンドを実行して、必要な 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),
        ),
      ),
    ];
  }
}

次に、同じ 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,
          ),
        ),
      ]),
    );
  }
}

3 つ目は、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 コンソールの [Authentication] に、新しいユーザーとしてメールアドレスが表示されます。

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

databaseURLfirebase_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 を利用して、1 人のユーザーがオンライン デバイスのリストを追跡します。次のコードを使用すると、この目標を達成できます。

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 コンソールで、データベース内の 1 つのユーザー ID の下にデバイスが表示されます。

5bef49cea3564248.png

6. デバイスの状態の同期

リード デバイスを選択

デバイス間で状態を同期するには、1 つのデバイスをリーダーまたはコントローラとして指定します。リードデバイスが、フォロワー デバイスの状態を決定します。

application_state.dartsetLeadDevice メソッドを作成し、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 ウィジェットを変更して Controller という PopupMenuItem を作成します。このメニューでリード デバイスを設定できます。

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. の末尾に追加します。これにより、プレーヤーの状態(再生または一時停止)とスライダーの位置の 2 つの属性が保存されるようになります。

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 でリーダー プレーヤーの状態のデータベース リスナーを設定します。このリスナーは、コールバックを介して画面を更新するタイミングをフォロワー デバイスに伝えます。このステップではインターフェース 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 Emulator でアプリを開きます。コンテキスト メニューに移動し、リーダーデバイスにするアプリを 1 つ選択します。リーダー デバイスが更新されると、フォロワー デバイスのプレーヤーが変更されます。
  3. 次に、リーダー デバイスを変更し、音楽を再生または一時停止して、フォロワー デバイスがそれに応じて更新されることを確認します。

フォロワー デバイスが正しく更新されたら、クロスデバイス コントローラが作成されました。残す作業は 1 つだけです。

7. セキュリティ ルールを更新する

より優れたセキュリティ ルールを作成しない限り、誰かが自分の所有していないデバイスに状態を書き込む可能性があります。そのため、完了する前に Realtime Database セキュリティ ルールを更新し、デバイスへの読み取りと書き込みを行えるユーザーが、そのデバイスにログインしているユーザーのみになるようにします。Firebase コンソールで、[Realtime Database] に移動し、[ルール] タブをクリックします。ログインしているユーザーのみが自分のデバイスの状態を読み取り、書き込めるようにする次のルールを貼り付けます。

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

8. 完了

bcd986f7106d892b.gif

お疲れさまでした。これで、Flutter を使用してクロスデバイス リモート コントローラを作成できました。

クレジット

Firebase の歌「Better Together」

  • ライアン・ヴァーノン音楽
  • 歌詞とアルバムカバー: Marissa Christy
  • ナレーション: JP Gomez

9. ボーナス

追加の課題として、Flutter FutureBuilder を使用して、現在のリードデバイスの種類を非同期で UI に追加することを検討してください。サポートが必要な場合は、Codelab の終了状態を含むフォルダに実装されています。

リファレンス ドキュメントと次のステップ