1. はじめに
最終更新日: 2022 年 3 月 14 日
クロスデバイス通信用の FlutterFire
ホーム オートメーション、ウェアラブル、パーソナル ヘルス テクノロジー デバイスが多数オンラインに接続されるにつれて、クロス デバイス通信はモバイル アプリケーションの構築においてますます重要な部分になっています。スマートフォンアプリからブラウザを操作したり、スマートフォンからテレビで再生するコンテンツを操作したりするなどのクロスデバイス通信の設定は、通常のモバイルアプリの構築よりも複雑です。
Firebase の Realtime Database には、ユーザーがデバイスのオンライン/オフライン ステータスを確認できる Presence API が用意されています。この API を Firebase Installations Service と組み合わせて使用すると、同じユーザーがログインしているすべてのデバイスを追跡して接続できます。Flutter を使用して複数のプラットフォーム用のアプリケーションをすばやく作成し、1 つのデバイスで音楽を再生し、別のデバイスで音楽を制御するクロスデバイス プロトタイプを構築します。
作成するアプリの概要
この Codelab では、シンプルな音楽プレーヤーのリモコンを作成します。作成するアプリの機能は次のとおりです。
- Flutter で構築された Android、iOS、ウェブ用のシンプルな音楽プレーヤー。
- ユーザーにログインを許可します。
- 複数のデバイスで同じユーザーがログインしている場合にデバイスを接続します。
- ユーザーが別のデバイスから 1 つのデバイスの音楽再生をコントロールできるようにします。

学習内容
- 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> コマンドでインストールできます。
ウェブアプリに、基本的なスタンドアロンの音楽プレーヤーが表示されます。プレーヤーの機能が想定どおりに動作していることを確認します。これは、この Codelab 用に設計されたシンプルな音楽プレーヤー アプリです。再生できるのは Firebase の曲「Better Together」のみです。
Android エミュレータまたは iOS シミュレータをセットアップする
開発用の Android デバイスまたは iOS デバイスがすでにある場合は、この手順をスキップできます。
Android エミュレータを作成するには、Flutter 開発もサポートしている Android Studio をダウンロードし、仮想デバイスの作成と管理の手順に沿って操作します。
iOS シミュレーターを作成するには、Mac 環境が必要です。XCode をダウンロードし、シミュレータの概要 > シミュレータを使用する > シミュレータを開閉するの手順に沿って操作します。
3. Firebase を設定する
Firebase プロジェクトを作成する
- Google アカウントを使用して Firebase コンソールにログインします。
- ボタンをクリックして新しいプロジェクトを作成し、プロジェクト名(例:
Firebase-Cross-Device-Codelab)を入力します。
- [続行] をクリックします。
- Firebase の利用規約が表示されたら、内容を読み、同意して [続行] をクリックします。
- (省略可)Firebase コンソールで AI アシスタンス(「Gemini in Firebase」)を有効にします。
- この Codelab では Google アナリティクスは必要ないため、Google アナリティクスのオプションをオフに切り替えます。
- [プロジェクトを作成] をクリックし、プロジェクトのプロビジョニングが完了するまで待ってから、[続行] をクリックします。
Firebase SDK をインストールする
コマンドラインに戻り、プロジェクト ディレクトリで次のコマンドを実行して Firebase をインストールします。
flutter pub add firebase_core
pubspec.yaml ファイルで、firebase_core のバージョンを 1.13.1 以上に編集するか、flutter upgrade を実行します。
FlutterFire を初期化する
- Firebase CLI がインストールされていない場合は、
curl -sL https://firebase.tools | bashを実行してインストールできます。 firebase loginを実行し、プロンプトに従ってログインします。dart pub global activate flutterfire_cliを実行して FlutterFire CLI をインストールします。flutterfire configureを実行して FlutterFire CLI を構成します。- プロンプトが表示されたら、この Codelab 用に作成したプロジェクト(Firebase-Cross-Device-Codelab など)を選択します。
- 構成サポートを選択するよう求められたら、[iOS]、[Android]、[ウェブ] を選択します。
- [Apple bundle ID] の入力を求められたら、一意のドメインを入力するか、
com.example.appnameを入力します。この Codelab の目的では、com.example.appnameで問題ありません。
構成が完了すると、初期化に必要なすべてのオプションを含む firebase_options.dart ファイルが生成されます。
エディタで、Flutter と Firebase を初期化するために、main.dart ファイルに次のコードを追加します。
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): プレゼンス API を使用してデバイスのオンライン/オフライン ステータスを追跡します。
- Firebase セキュリティ ルールを使用すると、データベースを保護できます。
- Firebase Installations Service: 1 人のユーザーがログインしたデバイスを識別します。
4. Firebase Auth を追加する
Firebase Authentication 用にメールアドレス ログインを有効にする
ユーザーがウェブアプリにログインできるようにするために、メール/パスワード ログインを使用します。
- Firebase コンソールの左側のパネルで、[ビルド] メニューを開きます。
- [認証]、[始める]、[ログイン方法] の順にクリックします。
- [ログイン プロバイダ] リストで [メール/パスワード] をクリックし、[有効にする] スイッチをオンの位置に設定して、[保存] をクリックします。

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 は同じままですが、ユーザーがログインしてアプリの状態を保存できるようになりました。
ログインフローを作成する
このステップでは、ログインとログアウトのフローを実装します。フローは次のようになります。
- ログアウトしたユーザーは、アプリバーの右側にあるコンテキスト メニュー
をクリックして、ログインフローを開始します。 - ログインフローがダイアログに表示されます。
- ユーザーがまだログインしたことがない場合は、有効なメールアドレスとパスワードを使用してアカウントを作成するよう求められます。
- 以前にログインしたことがある場合は、パスワードの入力が求められます。
- ユーザーがログインすると、コンテキスト メニューをクリックすると [ログアウト] オプションが表示されます。

ログインフローを追加するには、次の 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,
),
),
]),
);
}
}
次に、main.dart. AppBarMenuButton を追加して [ログイン] または [ログアウト] オプションを表示するで既存の appBar ウィジェットを見つけます。
lib/main.dart
import 'src/widgets.dart';
appBar: AppBar(
title: const Text('Music Box'),
backgroundColor: Colors.deepPurple.shade400,
actions: const <Widget>[
AppBarMenuButton(),
],
),
flutter run コマンドを実行して、これらの変更を適用してアプリを再起動します。アプリバーの右側にコンテキスト メニュー
が表示されます。クリックすると、ログイン ダイアログが表示されます。
有効なメールアドレスとパスワードでログインすると、コンテキスト メニューに [ログアウト] オプションが表示されます。
Firebase コンソールの [Authentication] で、新しいユーザーとしてメールアドレスが表示されていることを確認します。

これで完了です。これで、ユーザーはアプリにログインできるようになりました。
5. データベース接続を追加する
これで、Firebase Presence API を使用したデバイス登録に進む準備が整いました。
コマンドラインで、次のコマンドを実行して必要な依存関係を追加します。
flutter pub add firebase_app_installations
flutter pub add firebase_database
データベースの作成
Firebase コンソールで、
- Firebase コンソールの [Realtime Database] セクションに移動します。[データベースを作成] をクリックします。
- セキュリティ ルールの開始モードを選択するよう求められたら、とりあえず [テストモード] を選択します。(テストモードでは、すべてのリクエストを許可するセキュリティ ルールが作成されます。セキュリティ ルールは後で追加します。セキュリティ ルールがテストモードのまま本番環境に移行することは絶対に避けてください。
データベースは現在空です。[全般] タブの [プロジェクト設定] で databaseURL を確認します。[ウェブアプリ] セクションまで下にスクロールします。

firebase_options.dart ファイルに databaseURL を追加する:
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 の下にデバイスが表示されます。

6. デバイスの状態を同期する
リード デバイスを選択する
デバイス間で状態を同期するには、1 つのデバイスをリーダー(コントローラ)として指定します。リード デバイスがフォロワー デバイスの状態を決定します。
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 ウィジェットを変更して 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');
}
}
}
}
これで、アプリをテストする準備が整いました。
- コマンドラインで、
flutter run -d <device-name>を使用してエミュレータまたはブラウザでアプリを実行します。 - アプリをブラウザ、iOS シミュレータ、Android エミュレータで開きます。コンテキスト メニューに移動し、リーダー デバイスにするアプリを 1 つ選択します。リーダー デバイスが更新されると、フォロワー デバイスのプレーヤーが変化するのを確認できます。
- 次に、リーダー デバイスを変更し、音楽を再生または一時停止して、フォロワー デバイスがそれに応じて更新されることを確認します。
フォロワー デバイスが正しく更新されたら、クロスデバイス コントローラは正常に作成されています。重要なステップが 1 つ残っています。
7. セキュリティ ルールを更新する
セキュリティ ルールを改善しないと、所有していないデバイスに状態を書き込むことができてしまいます。そのため、終了する前に、Realtime Database セキュリティ ルールを更新して、デバイスの読み取りまたは書き込みができるユーザーが、そのデバイスにログインしているユーザーのみになるようにします。Firebase コンソールで、Realtime Database に移動し、[ルール] タブに移動します。ログインしたユーザーのみが自分のデバイスの状態を読み取り、書き込めるようにする次のルールを貼り付けます。
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. 完了

お疲れさまでした。これで、Flutter を使用してクロスデバイス リモコンを作成できました。
クレジット
Better Together, a Firebase Song
- ライアン バーノン音楽の作品
- 歌詞とアルバム カバー: Marissa Christy
- JP Gomez による音声
9. ボーナス
さらに、Flutter FutureBuilder を使用して、現在のリード デバイスタイプを UI に非同期で追加することも検討してください。アシストが必要な場合は、Codelab の完成状態を含むフォルダに実装されています。