Лаборатория кода Firebase для разных устройств

1. Введение

Последнее обновление: 14 марта 2022 г.

FlutterFire для связи между устройствами

Поскольку мы являемся свидетелями того, как большое количество устройств домашней автоматизации, носимых устройств и устройств для индивидуального здравоохранения подключаются к сети, взаимодействие между устройствами становится все более важной частью создания мобильных приложений. Настройка взаимодействия между устройствами, например управление браузером из приложения для телефона или управление тем, что воспроизводится на телевизоре с телефона, традиционно является более сложной задачей, чем создание обычного мобильного приложения.

База данных реального времени Firebase предоставляет API присутствия , который позволяет пользователям видеть статус своего устройства онлайн/оффлайн; вы будете использовать его со службой установки Firebase для отслеживания и подключения всех устройств, на которых вошел один и тот же пользователь. Вы будете использовать Flutter для быстрого создания приложений для нескольких платформ, а затем создадите прототип для нескольких устройств, который будет воспроизводиться музыку на одном устройстве и управлять музыкой на другом!

Что ты построишь

В этой лабораторной работе вы создадите простой пульт дистанционного управления музыкальным проигрывателем. Ваше приложение будет:

  • Создайте простой музыкальный проигрыватель на Android, iOS и в Интернете, созданный с помощью 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> .

Веб-приложение должно отображать базовый автономный музыкальный проигрыватель. Убедитесь, что функции плеера работают должным образом. Это простой музыкальный проигрыватель, разработанный для этой лаборатории. Он может воспроизводить только песню Firebase Better Together .

Настройте эмулятор Android или симулятор iOS.

Если у вас уже есть устройство Android или iOS для разработки, вы можете пропустить этот шаг.

Чтобы создать эмулятор Android, загрузите Android Studio , которая также поддерживает разработку Flutter, и следуйте инструкциям в разделе Создание виртуальных устройств и управление ими .

Для создания симулятора 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, вы можете установить его, запустив curl -sL https://firebase.tools | bash .
  2. Войдите в систему, запустив firebase login и следуя подсказкам.
  3. Установите интерфейс командной строки FlutterFire, запустив dart pub global activate flutterfire_cli .
  4. Настройте интерфейс командной строки FlutterFire, запустив flutterfire configure .
  5. В командной строке выберите проект, который вы только что создали для этой лаборатории кода, например Firebase-Cross-Device-Codelab .
  6. Когда вам будет предложено выбрать поддержку конфигурации, выберите iOS , Android и Интернет .
  7. Когда будет предложено ввести идентификатор пакета Apple , введите уникальный домен или введите 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

Вы еще не изменили какие-либо элементы пользовательского интерфейса, поэтому внешний вид и поведение приложения не изменились. Но теперь у вас есть приложение Firebase и вы можете начать использовать продукты Firebase, в том числе:

  • Firebase Authentication , которая позволяет вашим пользователям входить в ваше приложение.
  • База данных Firebase Realtime (RTDB) ; вы будете использовать API присутствия для отслеживания статуса устройства онлайн/оффлайн.
  • Правила безопасности Firebase позволят вам защитить базу данных.
  • Служба установки Firebase для идентификации устройств, на которые вошел один пользователь.

4. Добавьте аутентификацию Firebase

Включить вход по электронной почте для аутентификации Firebase

Чтобы разрешить пользователям входить в веб-приложение, вы будете использовать метод входа по электронной почте и паролю :

  1. В консоли Firebase разверните меню «Сборка» на левой панели.
  2. Нажмите «Аутентификация », затем нажмите кнопку «Начать» и выберите вкладку «Метод входа» .
  3. Нажмите «Электронная почта/пароль» в списке «Поставщики входа» , установите переключатель « Включить» во включенное положение, а затем нажмите «Сохранить» . 58e3e3e23c2f16a4.png

Настройка аутентификации 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(),
  ));
}

Опять же, пользовательский интерфейс приложения должен был остаться прежним, но теперь вы можете позволить пользователям входить в систему и сохранять состояния приложения.

Создайте вход в потоке

На этом этапе вы поработаете над процессом входа и выхода. Вот как будет выглядеть поток:

  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,
          ),
        ),
      ]),
    );
  }
}

В-третьих, найдите существующий виджет appBar в 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 . Нажмите Создать базу данных .
  2. Если будет предложено выбрать начальный режим для ваших правил безопасности, выберите сейчас тестовый режим **.** (Тестовый режим создает правила безопасности, которые разрешают все запросы. Вы добавите правила безопасности позже. Важно никогда не переходить к рабочей среде с ваши правила безопасности все еще находятся в тестовом режиме.)

База данных пока пуста. Найдите URL-адрес своей databaseURL в настройках проекта на вкладке «Общие» . Прокрутите вниз до раздела Веб-приложения .

1b6076f60a36263b.png

Добавьте URL-адрес своей databaseURL в файл firebase_options.dart :

lib/firebase_options.dart

 static const FirebaseOptions web = FirebaseOptions(
    apiKey: yourApiKey,
    ...
    databaseURL: 'https://<YOUR_DATABASE_URL>,
    ...
  );

Регистрируйте устройства с помощью API присутствия RTDB.

Вы хотите регистрировать устройства пользователя, когда они появляются в сети. Для этого вы воспользуетесь преимуществами установки Firebase и API присутствия Firebase RTDB, чтобы отслеживать список онлайн-устройств от одного пользователя. Следующий код поможет достичь этой цели:

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 вы должны увидеть, что ваши устройства отображаются под одним идентификатором пользователя в вашей базе данных.

5bef49cea3564248.png

6. Синхронизация состояния устройства

Выберите ведущее устройство

Чтобы синхронизировать состояния между устройствами, назначьте одно устройство ведущим или контроллером. Ведущее устройство будет определять состояния ведомых устройств.

Создайте метод setLeadDevice в application_state.dart и отслеживайте это устройство с помощью ключа active_device в RTDB:

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

Чтобы добавить эту функцию в контекстное меню панели приложения, создайте PopupMenuItem с именем Controller , изменив виджет SignedInMenuButton . Это меню позволит пользователям настроить ведущее устройство.

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

  • Музыка Райана Вернона
  • Слова и обложка альбома Мариссы Кристи.
  • Голос Джей Пи Гомеса

9. Бонус

В качестве дополнительной задачи рассмотрите возможность использования Flutter FutureBuilder для асинхронного добавления текущего типа ведущего устройства в пользовательский интерфейс. Если вам нужна помощь, она реализована в папке, содержащей готовое состояние кодовой лаборатории.

Справочная документация и следующие шаги