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

1. Введение

Последнее обновление: 14.03.2022

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

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

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

Что вы построите

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

  • Создайте простой музыкальный проигрыватель для Android, iOS и веб-браузеров, созданный с помощью Flutter.
  • Разрешить пользователям входить в систему.
  • Подключайте устройства, когда один и тот же пользователь вошел в систему на нескольких устройствах.
  • Разрешить пользователям управлять воспроизведением музыки на одном устройстве с другого устройства.

7f0279938e1d3ab5.gif

Чему вы научитесь

  • Как создать и запустить приложение музыкального проигрывателя 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, загрузите Android Studio , которая также поддерживает разработку Flutter, и следуйте инструкциям в разделе Создание и управление виртуальными устройствами .

Для создания симулятора iOS вам понадобится среда Mac. Загрузите XCode и следуйте инструкциям в разделе Обзор симулятора > Использование симулятора > Открытие и закрытие симулятора .

3. Настройте Firebase

Создать проект Firebase

  1. Войдите в консоль Firebase, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите имя проекта (например, Firebase-Cross-Device-Codelab ).
  3. Нажмите «Продолжить» .
  4. При появлении соответствующего запроса ознакомьтесь с условиями Firebase и примите их, а затем нажмите кнопку «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (так называемая «Gemini в Firebase»).
  6. Для этой лабораторной работы вам не понадобится Google Analytics, поэтому отключите опцию Google Analytics.
  7. Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .

Установить 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 .
  2. Для входа в систему выполните firebase login и следуйте инструкциям.
  3. Установите FlutterFire CLI, выполнив команду dart pub global activate flutterfire_cli .
  4. Настройте FlutterFire CLI, выполнив flutterfire configure .
  5. В приглашении выберите проект, который вы только что создали для этой кодовой лаборатории, например Firebase-Cross-Device-Codelab .
  6. При появлении запроса на выбор поддержки конфигурации выберите iOS , Android и Web .
  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 , которая позволяет пользователям входить в ваше приложение.
  • База данных Firebase Realtime Database (RTDB) ; вы будете использовать API присутствия для отслеживания статуса устройства (онлайн/офлайн)
  • Правила безопасности Firebase позволят вам защитить базу данных.
  • Служба установок Firebase для идентификации устройств, на которых выполнил вход отдельный пользователь.

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

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

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

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

Настройка аутентификации Firebase во Flutter

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

База данных пока пуста. Найдите 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 Presence

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

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!

Кредиты

Better Together, песня Firebase

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

9. Бонус

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

Справочные документы и дальнейшие шаги