Firebase 跨设备代码实验室

一、简介

最后更新: 2022-03-14

FlutterFire 用于跨设备通信

随着我们见证大量家庭自动化、可穿戴和个人健康技术设备上线,跨设备通信成为构建移动应用程序日益重要的一部分。设置跨设备通信,例如通过手机应用程序控制浏览器,或通过手机控制电视上播放的内容,传统上比构建普通的移动应用程序更复杂。

Firebase的实时数据库提供了Presence API ,允许用户查看他们的设备在线/离线状态;您将使用它与 Firebase Installations Service 一起跟踪和连接同一用户登录的所有设备。您将使用 Flutter 快速创建适用于多个平台的应用程序,然后构建一个可运行的跨设备原型在一台设备上播放音乐并控制另一台设备上的音乐!

你将构建什么

在此 Codelab 中,您将构建一个简单的音乐播放器遥控器。您的应用程序将:

  • 在 Android、iOS 和 Web 上拥有一个使用 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 模拟器或 iOS 模拟器,则可以为这些平台构建应用程序并使用命令flutter run -d <device_name>安装它。

网络应用程序应显示一个基本的独立音乐播放器。确保播放器功能按预期工作。这是为此 Codelab 设计的简单音乐播放器应用。它只能播放 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 条款。跳过设置 Google Analytics,因为您不会为此应用程序使用 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. 根据提示,选择您刚刚为此 Codelab 创建的项目,例如Firebase-Cross-Device-Codelab
  6. 当系统提示您选择配置支持时,选择iOSAndroidWeb
  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 身份验证,允许您的用户登录您的应用。
  • Firebase 实时数据库(RTDB) ;您将使用状态 API 来跟踪设备在线/离线状态
  • Firebase 安全规则将让您保护数据库。
  • Firebase Installations 服务用于识别单个用户已登录的设备。

4. 添加 Firebase 身份验证

启用电子邮件登录以进行 Firebase 身份验证

要允许用户登录 Web 应用程序,您将使用电子邮件/密码登录方法:

  1. 在 Firebase 控制台中,展开左侧面板中的“构建”菜单。
  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

添加登录流程需要三个步骤。

首先,创建一个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以显示登录注销选项。

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 控制台“实时数据库”部分。单击创建数据库
  2. 如果提示选择安全规则的启动模式,请立即选择测试模式**。**(测试模式创建允许所有请求通过的安全规则。稍后您将添加安全规则。重要的是永远不要将其投入生产您的安全规则仍处于测试模式。)

数据库目前为空。在“项目设置”的“常规”选项卡下找到您的databaseURL 。向下滚动到Web 应用程序部分。

1b6076f60a36263b.png

将您的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 下。

5bef49cea3564248.png

6. 同步设备状态

选择主导设备

要同步设备之间的状态,请将一台设备指定为领导者或控制器。主导设备将决定跟随设备的状态。

application_state.dart中创建setLeadDevice方法,并使用 RTDB 中的 key 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小部件创建一个名为ControllerPopupMenuItem 。该菜单允许用户设置主导设备。

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');
        }
      }
    }
  }

现在您已准备好测试该应用程序:

  1. 在命令行上,使用以下命令在模拟器和/或浏览器中运行应用程序: flutter run -d <device-name>
  2. 在浏览器、iOS 模拟器或 Android 模拟器上打开应用程序。转到上下文菜单,选择一个应用程序作为主要设备。您应该能够看到跟随者设备的玩家随着领导者设备的更新而变化。
  3. 现在更改领导设备,播放或暂停音乐,并观察跟随设备相应更新。

如果跟随设备更新正常,则说明您已经成功制作跨设备控制器。只剩下关键的一步了。

7. 更新安全规则

除非我们编写更好的安全规则,否则有人可以将状态写入他们不拥有的设备!因此,在完成之前,请更新实时数据库安全规则,以确保登录该设备的用户是唯一可以读取或写入设备的用户。在 Firebase 控制台中,导航到实时数据库,然后导航到“规则”选项卡。粘贴以下规则,仅允许登录用户读取和写入自己的设备状态:

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

8. 恭喜!

bcd986f7106d892b.gif

恭喜您已经成功使用 Flutter 构建了跨设备远程控制器!

制作人员

更好地在一起,Firebase 歌曲

  • 瑞安·弗农的音乐
  • 玛丽莎·克里斯蒂 (Marissa Christy) 作词及专辑封面
  • JP 戈麦斯 配音

9. 奖金

作为一项额外的挑战,请考虑使用 Flutter FutureBuilder将当前主导设备类型异步添加到 UI。如果您需要帮助,可以在包含 Codelab 完成状态的文件夹中实现。

参考文档和后续步骤