1. 簡介
上次更新時間:2022 年 3 月 14 日
適用於跨裝置通訊的 FlutterFire
我們發現許多家用裝置自動化、穿戴式裝置和個人健康科技裝置紛紛出現在網路上,因此在建構行動應用程式的過程中,跨裝置通訊變得越來越重要。與一般的行動應用程式相比,設定跨裝置通訊 (例如透過手機應用程式控制瀏覽器或控管電視播放內容) 通常較為複雜。
Firebase 的即時資料庫提供 Presence API ,讓使用者查看裝置的上線/離線狀態。將與 Firebase Installations Service 搭配使用,即可追蹤及連結同一個使用者已登入的所有裝置。您將使用 Flutter 快速建立多個平台的應用程式,然後建構跨裝置原型,在一部裝置上播放音樂,然後在另一部裝置上控制音樂!
建構項目
在本程式碼研究室中,您將建構一個簡單的音樂播放器遙控器。您的應用程式將會:
- 您在 Android、iOS 和網路上使用簡單好用的音樂播放器,並採用 Flutter 建構而成。
- 允許使用者登入。
- 在使用者於多部裝置上登入時連結裝置。
- 允許使用者透過另一部裝置控制在某部裝置上的音樂播放。
課程內容
- 如何建構並執行 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/。
- 登入 Firebase。
- 在 Firebase 控制台中,按一下「新增專案」 (或「建立專案」),然後將 Firebase 專案命名為「Firebase-Cross-Device-Codelab」。
- 點選專案建立選項。如果系統顯示提示,請接受 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
- 如果您未安裝 Firebase CLI,可以執行
curl -sL https://firebase.tools | bash
進行安裝。 - 執行
firebase login
並按照提示登入。 - 執行
dart pub global activate flutterfire_cli
以安裝 FlutterFire CLI。 - 執行
flutterfire configure
來設定 FlutterFire CLI。 - 在提示中,選擇剛剛為本程式碼研究室建立的專案,例如 Firebase-Cross-Device-Codelab。
- 當系統提示您選擇設定支援服務時,請選取「iOS」、「Android」和「網路」。
- 系統提示您輸入 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 驗證啟用電子郵件登入功能
如要允許使用者登入網頁應用程式,請使用「電子郵件/密碼」登入方式:
- 在 Firebase 控制台中,展開左側面板中的「建構」選單。
- 點選「驗證」,然後依序點選「開始使用」按鈕和「登入方式」分頁標籤。
- 按一下「登入供應商」清單中的「電子郵件/密碼」,將「啟用」的切換按鈕設為開啟,然後按一下「儲存」。
在 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 應維持不變,但您現在可以讓使用者登入並儲存應用程式狀態。
建立登入流程
在這個步驟中,您將處理登入和登出流程。流程大致如下:
- 已登出的使用者按一下應用程式列右側的內容選單 ,即可啟動登入流程。
- 系統會在對話方塊中顯示登入流程。
- 如果使用者從未登入,系統會提示他們使用有效的電子郵件地址和密碼建立帳戶。
- 如果使用者先前曾登入,系統會提示他們輸入密碼。
- 使用者登入後,按一下內容選單就會顯示 [登出] 選項。
新增登入流程需要三個步驟。
首先,建立 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
指令,使用這些變更重新啟動應用程式。您應該可以在應用程式列的右側看到內容選單 。按一下該選項即可開啟登入對話方塊。
使用有效的電子郵件地址和密碼登入帳戶後,內容選單中應該會顯示「登出」選項。
在 Firebase 控制台的「驗證」下方,您應該會看到列為新使用者的電子郵件地址。
恭喜!使用者現在可以登入應用程式了!
5. 新增資料庫連線
您現在準備好使用 Firebase Presence API 註冊裝置了。
在指令列中執行下列指令,新增必要的依附元件:
flutter pub add firebase_app_installations
flutter pub add firebase_database
建立資料庫
在 Firebase 控制台中
- 前往 Firebase 控制台的「即時資料庫」部分。按一下「建立資料庫」。
- 如果系統提示您為安全性規則選取啟動模式,請暫時選取「Test Mode」。**(測試模式會建立安全性規則,允許所有要求通過,您稍後會新增安全性規則。在測試模式中,您的安全性規則一律不應進入正式環境)。
資料庫目前沒有任何內容。在「General」分頁下方的「Project settings」中找到您的 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 安裝功能和 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 底下。
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
小工具,建立名為 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.
這麼做會開始儲存兩個屬性:玩家狀態 (播放或暫停) 和滑桿位置。
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');
}
}
}
}
您現在可以測試應用程式:
- 在指令列中,使用
flutter run -d <device-name>
在模擬器和/或瀏覽器中執行應用程式: - 使用瀏覽器、iOS 模擬器或 Android 模擬器開啟應用程式。前往內容選單,選擇要做為主要裝置的應用程式。你應該可以看到追蹤者的裝置」隨著主要裝置的更新而改變
- 現在變更主要裝置、播放或暫停音樂,並觀察追蹤者裝置是否有相應更新。
如果追蹤者裝置正確更新,表示您已成功建立跨裝置控制器。只剩一個關鍵的步驟。
7. 更新安全性規則
除非我們編寫了更好的安全性規則,否則有心人士可以將狀態寫入非自己的裝置!因此在完成前,請更新即時資料庫安全性規則,確保只有已登入該裝置的使用者才能讀取或寫入裝置。在 Firebase 控制台中,依序前往「即時資料庫」和「規則」分頁。請貼上下列規則,只允許已登入的使用者讀取及寫入自己的裝置狀態:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. 恭喜!
恭喜,您已成功使用 Flutter 建構跨裝置遠端控制器!
抵免額
發揮 Firebase 的最大效益
- 萊恩維農音樂
- 歌詞和專輯封面 Marissa Christy
- Voice by JP Gomez
9. 獎金
此外,建議您使用 Flutter FutureBuilder
,將目前的待開發客戶裝置類型以非同步方式新增至 UI。如需相關協助,我們會將其導入包含程式碼研究室完成狀態的資料夾。