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

1. Введение

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

FlutterFile для межплатформенной связи

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

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

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

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

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

7f0279938e1d3ab5.gif

Что вы узнаете

  • Как создать и запустить приложение-музыкальный плеер на Flutter.
  • Как разрешить пользователям входить в систему с помощью Firebase Auth.
  • Как использовать API Firebase RTDB Presence и службу установки 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.

  1. Войдите в консоль Firebase, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например, Firebase-Cross-Device-Codelab ).
  3. Нажмите «Продолжить» .
  4. Если появится запрос, ознакомьтесь с условиями использования Firebase и примите их, после чего нажмите «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (в Firebase она называется "Gemini").
  6. Для этого практического занятия вам не понадобится 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, выполнив 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 Authentication , которая позволяет пользователям входить в ваше приложение.
  • База данных Firebase Realtime Database (RTDB) ; вы будете использовать API присутствия для отслеживания статуса устройства (онлайн/офлайн).
  • Правила безопасности Firebase позволят вам защитить базу данных.
  • Сервис Firebase Installations используется для идентификации устройств, к которым подключился один пользователь.

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 пользователя (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. Добавьте подключение к базе данных.

Теперь вы готовы перейти к регистрации устройства с помощью API Firebase Presence.

В командной строке выполните следующие команды для добавления необходимых зависимостей:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Создайте базу данных

В консоли Firebase,

  1. Перейдите в раздел «База данных реального времени» в консоли Firebase . Нажмите «Создать базу данных» .
  2. Если появится запрос на выбор начального режима для ваших правил безопасности, выберите пока тестовый режим . (Тестовый режим создает правила безопасности, которые разрешают все запросы. Правила безопасности вы добавите позже. Важно никогда не запускать систему в рабочую среду, если ваши правила безопасности все еще находятся в тестовом режиме.)

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

1b6076f60a36263b.png

Добавьте ваш databaseURL в файл firebase_options.dart :

lib/firebase_options.dart

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

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

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

Если мы не напишем более совершенные правила безопасности, кто-то сможет записать состояние на устройство, которое ему не принадлежит! Поэтому, прежде чем закончить, обновите правила безопасности базы данных Realtime Database, чтобы убедиться, что читать или записывать данные на устройство могут только пользователи, вошедшие в систему на этом устройстве. В консоли Firebase перейдите в раздел Realtime Database, а затем на вкладку «Правила» . Вставьте следующие правила, разрешающие чтение и запись состояний своего устройства только вошедшим в систему пользователям:

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

8. Поздравляем!

bcd986f7106d892b.gif

Поздравляем, вы успешно создали кроссплатформенный пульт дистанционного управления с помощью Flutter!

Кредиты

Better Together, песня Firebase

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

9. Бонус

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

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