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 インストール サービスを使用してデバイスを接続する方法。
必要なもの
- 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/ にアクセスします。
- Firebase にログインします。
- Firebase コンソールで [プロジェクトを追加](または [プロジェクトを作成])をクリックし、Firebase プロジェクトに Firebase-Cross-Device-Codelab という名前を付けます。
- プロジェクト作成オプションをクリックします。プロンプトが表示されたら、Firebase の利用規約に同意します。このアプリではアナリティクスを使用しないため、Google アナリティクスの設定はスキップします。
上記のファイルをダウンロードしたり、build.gradle ファイルを変更したりする必要はありません。これらの設定は、FlutterFire の初期化時に行います。
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 バンドル 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 Installations Service: 1 人のユーザーがログインしたデバイスを識別します。
4. Firebase Auth を追加する
Firebase Authentication でメール認証を有効にする
ユーザーがウェブアプリにログインできるようにするには、メール/パスワードのログイン方法を使用します。
- Firebase コンソールの左側のパネルで [Build] メニューを開きます。
- [認証]、[使ってみる] ボタン、[ログイン方法] タブの順にクリックします。
- [ログイン プロバイダ] リストで [メール/パスワード] をクリックし、[有効にする] スイッチをオンの位置に設定して、[保存] をクリックします。
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,
),
),
]),
);
}
}
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
コマンドを実行して、変更を加えたアプリを再起動します。アプリバーの右側にコンテキスト メニュー が表示されます。クリックするとログイン ダイアログが表示されます。
有効なメールアドレスとパスワードでログインすると、コンテキスト メニューに [ログアウト] オプションが表示されます。
Firebase コンソールの [Authentication] で、メールアドレスが新しいユーザーとして表示されます。
これで完了です。これで、ユーザーはアプリにログインできるようになります。
5. データベース接続を追加する
これで、Firebase Presence API を使用したデバイス登録に進むことができます。
コマンドラインで次のコマンドを実行して、必要な依存関係を追加します。
flutter pub add firebase_app_installations
flutter pub add firebase_database
データベースの作成
Firebase コンソールで、
- Firebase コンソールの [Realtime Database] セクションに移動します。[データベースを作成] をクリックします。
- セキュリティ ルールの開始モードを選択するよう求められたら、今のところ [テストモード] を選択します。(テストモードでは、すべてのリクエストを許可するセキュリティ ルールが作成されます。セキュリティ ルールは後で追加します。セキュリティ ルールがテストモードのまま本番環境に移行しないことが重要です)。
現時点ではデータベースは空です。[プロジェクトの設定] の [全般] タブで databaseURL
を見つけます。[ウェブアプリ] セクションまでスクロールします。
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 を使用して、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 Emulator でアプリを開きます。コンテキスト メニューに移動し、リーダー デバイスとなるアプリを 1 つ選択します。リーダー デバイスが更新されると、フォロワー デバイスのプレーヤーが変更されます。
- 次に、リーダー デバイスを変更し、音楽を再生または一時停止して、フォロワー デバイスがそれに応じて更新されることを確認します。
フォロワー デバイスが正しく更新されたら、クロスデバイス コントローラが作成されています。残す作業は 1 つだけです。
7. セキュリティ ルールを更新する
より優れたセキュリティ ルールを記述しない限り、誰かが所有していないデバイスに状態を書き込む可能性があります。そのため、完了する前に Realtime Database セキュリティ ルールを更新し、デバイスへの読み取りと書き込みを行えるユーザーが、そのデバイスにログインしているユーザーのみになるようにします。Firebase コンソールで Realtime Database に移動し、[ルール] タブに移動します。ログインしているユーザーのみが自分のデバイスの状態を読み取り、書き込めるようにする次のルールを貼り付けます。
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. 完了
お疲れさまでした。これで、Flutter を使用してクロスデバイス リモート コントローラを作成できました。
クレジット
Firebase の楽曲「Better Together」
- 音楽: ライアン バーノン
- 歌詞とアルバムカバー: Marissa Christy
- ナレーション: JP Gomez
9. ボーナス
追加の課題として、Flutter FutureBuilder
を使用して、現在のリードデバイスの種類を非同期で UI に追加することを検討してください。サポートが必要な場合は、Codelab の完成状態が格納されているフォルダに実装されています。