Firebase クロスデバイス コードラボ

1. はじめに

最終更新日: 2022-03-14

FlutterFire によるクロスデバイス通信

多数のホーム オートメーション、ウェアラブル、パーソナル ヘルス テクノロジー デバイスがオンラインになるにつれ、クロスデバイス通信はモバイル アプリケーションを構築する上でますます重要な部分になってきています。電話アプリからブラウザを制御したり、テレビで再生される内容を電話から制御したりするなど、クロスデバイス通信を設定することは、従来、通常のモバイル アプリを構築するよりも複雑です。

Firebase の Realtime Database は、ユーザーがデバイスのオンライン/オフライン ステータスを確認できるようにするPresence APIを提供します。これを Firebase Installations Service と併用して、同じユーザーがサインインしているすべてのデバイスを追跡して接続します。Flutter を使用して複数のプラットフォーム用のアプリケーションをすばやく作成し、次に再生するクロスデバイス プロトタイプを構築します。あるデバイスの音楽を別のデバイスでコントロールできます。

何を構築するか

このコードラボでは、シンプルな音楽プレーヤーのリモコンを作成します。あなたのアプリは次のことを行います:

  • Flutter で構築された、Android、iOS、Web 上のシンプルな音楽プレーヤーを用意します。
  • ユーザーにサインインを許可します。
  • 同じユーザーが複数のデバイスにサインインしている場合にデバイスを接続します。
  • ユーザーがあるデバイスでの音楽再生を別のデバイスから制御できるようにします。

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>を使用してインストールできます。

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

Android エミュレータまたは 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 の利用規約に同意します。このアプリでは Analytics を使用しないため、Google 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を実行してインストールできます。 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」、および「Web」を選択します。
  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 Authentication : ユーザーがアプリにサインインできるようにします。
  • Firebase リアルタイム データベース (RTDB) ;プレゼンス API を使用してデバイスのオンライン/オフライン ステータスを追跡します
  • Firebase セキュリティ ルールを使用すると、データベースを保護できます。
  • 単一ユーザーがサインインしているデバイスを識別するFirebase インストール サービス

4. Firebase認証を追加する

Firebase Authentication のメール サインインを有効にする

ユーザーが Web アプリにサインインできるようにするには、電子メール/パスワードによるサインイン方法を使用します。

  1. Firebase コンソールで、左側のパネルの[Build]メニューを展開します。
  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

サインイン フローを追加するには、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. 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を見つけます。 「Web アプリ」セクションまで下にスクロールします。

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 インストールと 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 コンソールでは、データベース内の 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 エミュレーターでアプリを開きます。コンテキスト メニューに移動し、リーダー デバイスとなるアプリを 1 つ選択します。リーダー デバイスが更新されると、フォロワー デバイスのプレーヤーが変更されるのが確認できるはずです。
  3. 次に、リーダー デバイスを変更し、音楽を再生または一時停止し、それに応じてフォロワー デバイスが更新されるのを観察します。

フォロワーデバイスが適切に更新されれば、クロスデバイスコントローラーの作成は成功です。重要なステップがあと 1 つだけ残っています。

7. セキュリティルールの更新

より良いセキュリティ ルールを作成しない限り、誰かが自分が所有していないデバイスに状態を書き込む可能性があります。したがって、完了する前に、リアルタイム データベース セキュリティ ルールを更新して、デバイスの読み取りまたは書き込みができるのは、そのデバイスにサインインしているユーザーだけであることを確認してください。 Firebase コンソールで、Realtime Database に移動し、 [ルール]タブに移動します。サインインしているユーザーのみが自分のデバイス状態の読み取りと書き込みを許可する次のルールを貼り付けます。

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

8. おめでとうございます!

bcd986f7106d892b.gif

おめでとうございます。Flutter を使用してクロスデバイス リモート コントローラーを構築することに成功しました。

クレジット

Better Together、Firebase ソング

  • 音楽:ライアン・バーノン
  • マリッサ・クリスティによる歌詞とアルバムカバー
  • 声:JP・ゴメス

9. ボーナス

追加の課題として、Flutter FutureBuilderを使用して、現在のリード デバイス タイプを UI に非同期的に追加することを検討してください。アシストが必要な場合は、コードラボの完成状態を含むフォルダーに実装されています。

参考ドキュメントと次のステップ