Laboratorio de programación de dispositivos cruzados de Firebase

1. Introducción

Última actualización: 2022-03-14

FlutterFire para la comunicación entre dispositivos

A medida que somos testigos de una gran cantidad de dispositivos de tecnología de salud personal, portátiles y de automatización del hogar que se conectan, la comunicación entre dispositivos se convierte en una parte cada vez más importante de la creación de aplicaciones móviles. Configurar la comunicación entre dispositivos, como controlar un navegador desde una aplicación de teléfono o controlar lo que se reproduce en su televisor desde su teléfono, es tradicionalmente más complejo que crear una aplicación móvil normal.

La base de datos en tiempo real de Firebase proporciona la API de presencia que permite a los usuarios ver el estado en línea/fuera de línea de su dispositivo; lo usará con Firebase Installations Service para rastrear y conectar todos los dispositivos en los que el mismo usuario haya iniciado sesión. Usará Flutter para crear rápidamente aplicaciones para múltiples plataformas, y luego creará un prototipo de dispositivo cruzado que reproduce música en un dispositivo y controla la música en otro!

lo que vas a construir

En este laboratorio de programación, construirá un control remoto simple para un reproductor de música. Tu aplicación:

  • Tenga un reproductor de música simple en Android, iOS y web, creado con Flutter.
  • Permitir que los usuarios inicien sesión.
  • Conecte dispositivos cuando el mismo usuario haya iniciado sesión en varios dispositivos.
  • Permita que los usuarios controlen la reproducción de música en un dispositivo desde otro dispositivo.

7f0279938e1d3ab5.gif

lo que aprenderás

  • Cómo crear y ejecutar una aplicación de reproductor de música Flutter.
  • Cómo permitir que los usuarios inicien sesión con Firebase Auth.
  • Cómo usar Firebase RTDB Presence API y Firebase Installation Service para conectar dispositivos.

Lo que necesitarás

  • Un entorno de desarrollo de Flutter. Siga las instrucciones en la guía de instalación de Flutter para configurarlo.
  • Se requiere una versión mínima de Flutter de 2.10 o superior. Si tiene una versión anterior, ejecute flutter upgrade.
  • Una cuenta de Firebase.

2. Preparación

Obtener el código de inicio

Hemos creado una aplicación de reproductor de música en Flutter. El código de inicio se encuentra en un repositorio de Git. Para comenzar, en la línea de comando, clone el repositorio, muévase a la carpeta con el estado inicial e instale las dependencias:

git clone https://github.com/FirebaseExtended/cross-device-controller.git

cd cross-device-controller/starter_code

flutter pub get

Crea la aplicación

Puede trabajar con su IDE favorito para crear la aplicación o usar la línea de comandos.

En el directorio de tu aplicación, compila la aplicación para web con el comando flutter run -d web-server. Debería poder ver el siguiente mensaje.

lib/main.dart is being served at http://localhost:<port>

Visite http://localhost:<port> para ver el reproductor de música.

Si está familiarizado con el emulador de Android o el simulador de iOS, puede crear la aplicación para esas plataformas e instalarla con el comando flutter run -d <device_name> .

La aplicación web debe mostrar un reproductor de música independiente básico. Asegúrese de que las características del reproductor funcionen según lo previsto. Esta es una aplicación de reproductor de música simple diseñada para este laboratorio de código. Solo puede reproducir una canción de Firebase, Better Together .

Configurar un emulador de Android o un simulador de iOS

Si ya tiene un dispositivo Android o un dispositivo iOS para desarrollo, puede omitir este paso.

Para crear un emulador de Android, descargue Android Studio , que también es compatible con el desarrollo de Flutter, y siga las instrucciones en Crear y administrar dispositivos virtuales .

Para crear un simulador de iOS, necesitará un entorno Mac. Descargue XCode y siga las instrucciones en Descripción general del simulador > Usar simulador > Abrir y cerrar un simulador .

3. Configuración de Firebase

Crear un proyecto de Firebase

Abra un navegador en http://console.firebase.google.com/ .

  1. Inicie sesión en Firebase .
  2. En la consola de Firebase, haga clic en Agregar proyecto (o Crear un proyecto ) y asigne a su proyecto de Firebase el nombre Firebase-Cross-Device-Codelab .
  3. Haga clic en las opciones de creación de proyectos. Acepte los términos de Firebase si se le solicita. Omita la configuración de Google Analytics, porque no usará Analytics para esta aplicación.

No necesita descargar los archivos mencionados o cambiar los archivos build.gradle. Los configurará cuando inicie FlutterFire.

Instalar SDK de Firebase

De vuelta en la línea de comandos, en el directorio del proyecto, ejecute el siguiente comando para instalar Firebase:

flutter pub add firebase_core

En el archivo pubspec.yaml , edite la versión de firebase_core para que sea al menos 1.13.1 o ejecute flutter upgrade

Inicializar FlutterFire

  1. Si no tiene Firebase CLI instalado, puede instalarlo ejecutando curl -sL https://firebase.tools | bash
  2. Inicie sesión ejecutando firebase login y siguiendo las indicaciones.
  3. Instale FlutterFire CLI ejecutando dart pub global activate flutterfire_cli .
  4. Configure la CLI de FlutterFire ejecutando flutterfire configure .
  5. Cuando se le solicite, elija el proyecto que acaba de crear para este codelab, algo así como Firebase-Cross-Device-Codelab .
  6. Seleccione iOS , Android y Web cuando se le solicite que elija soporte de configuración.
  7. Cuando se le solicite el ID del paquete de Apple , escriba un dominio único o ingrese com.example.appname , lo cual está bien para este codelab.

Una vez configurado, se generará un archivo firebase_options.dart que contiene todas las opciones necesarias para la inicialización.

En su editor, agregue el siguiente código a su archivo main.dart para inicializar Flutter y 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());
}

Compile la aplicación con el comando:

flutter run

Todavía no ha cambiado ningún elemento de la interfaz de usuario, por lo que el aspecto y el comportamiento de la aplicación no han cambiado. Pero ahora tiene una aplicación de Firebase y puede comenzar a usar los productos de Firebase, incluidos:

  • Firebase Authentication , que permite a sus usuarios iniciar sesión en su aplicación.
  • Base de datos en tiempo real de Firebase (RTDB) ; utilizará la API de presencia para rastrear el estado en línea/fuera de línea del dispositivo
  • Las reglas de seguridad de Firebase le permitirán proteger la base de datos.
  • Firebase Installations Service para identificar los dispositivos en los que un solo usuario ha iniciado sesión.

4. Agregar autenticación de Firebase

Habilitar el inicio de sesión por correo electrónico para la autenticación de Firebase

Para permitir que los usuarios inicien sesión en la aplicación web, utilizará el método de inicio de sesión con correo electrónico/contraseña :

  1. En Firebase console, expanda el menú Build en el panel izquierdo.
  2. Haga clic en Autenticación y, a continuación, haga clic en el botón Comenzar y luego en la pestaña Método de inicio de sesión .
  3. Haga clic en Correo electrónico/Contraseña en la lista de proveedores de inicio de sesión , configure el interruptor Habilitar en la posición de encendido y luego haga clic en Guardar . 58e3e3e23c2f16a4.png

Configurar la autenticación de Firebase en Flutter

En la línea de comandos, ejecuta los siguientes comandos para instalar los paquetes flutter necesarios:

flutter pub add firebase_auth

flutter pub add provider

Con esta configuración, ahora puede crear el flujo de inicio y cierre de sesión. Dado que el estado de autenticación no debe cambiar de una pantalla a otra, creará una clase application_state.dart para realizar un seguimiento de los cambios de estado a nivel de la aplicación, como iniciar y cerrar sesión. Obtenga más información sobre esto en la documentación de administración de estado de Flutter .

Pegue lo siguiente en el nuevo archivo 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();
  }
}

Para asegurarse de que ApplicationState se inicialice cuando se inicie la aplicación, agregará un paso de inicialización a 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(),
  ));
}

Una vez más, la interfaz de usuario de la aplicación debería haber permanecido igual, pero ahora puede permitir que los usuarios inicien sesión y guarden los estados de la aplicación.

Crear un flujo de inicio de sesión

En este paso, trabajará en el flujo de inicio y cierre de sesión. Así es como se verá el flujo:

  1. Un usuario desconectado iniciará el flujo de inicio de sesión haciendo clic en el menú contextual 71fcc1030a336423.png en el lado derecho de la barra de la aplicación.
  2. El flujo de inicio de sesión se mostrará en un cuadro de diálogo.
  3. Si el usuario nunca ha iniciado sesión antes, se le pedirá que cree una cuenta con una dirección de correo electrónico válida y una contraseña.
  4. Si el usuario ha iniciado sesión anteriormente, se le pedirá que ingrese su contraseña.
  5. Una vez que el usuario haya iniciado sesión, al hacer clic en el menú contextual se mostrará la opción Cerrar sesión .

c295f6fa2e1d40f3.png

Agregar un flujo de inicio de sesión requiere tres pasos.

En primer lugar, cree un widget AppBarMenuButton . Este widget controlará la ventana emergente del menú contextual según el loginState de un usuario. Agrega las importaciones

lib/src/widgets.dart

import 'application_state.dart';
import 'package:provider/provider.dart';
import 'authentication.dart';

Agregue el siguiente código a 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),
        ),
      ),
    ];
  }
}

En segundo lugar, en la misma clase widgets.dart , cree el widget 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,
          ),
        ),
      ]),
    );
  }
}

En tercer lugar, busque el widget appBar existente en main.dart. Agregue AppBarMenuButton para mostrar la opción Iniciar sesión o Cerrar sesión .

lib/main.dart

import 'src/widgets.dart';
appBar: AppBar(
  title: const Text('Music Box'),
  backgroundColor: Colors.deepPurple.shade400,
  actions: const <Widget>[
    AppBarMenuButton(),
  ],
),

Ejecute el comando flutter run para reiniciar la aplicación con estos cambios. Debería poder ver el menú contextual. 71fcc1030a336423.png en el lado derecho de la barra de la aplicación. Al hacer clic en él, accederá a un cuadro de diálogo de inicio de sesión.

Una vez que inicie sesión con una dirección de correo electrónico válida y una contraseña, debería poder ver la opción Cerrar sesión en el menú contextual.

En la consola de Firebase, en Autenticación , debería poder ver la dirección de correo electrónico que aparece como un nuevo usuario.

888506c86a28a72c.png

¡Felicidades! ¡Los usuarios ahora pueden iniciar sesión en la aplicación!

5. Agregar conexión de base de datos

Ahora está listo para continuar con el registro del dispositivo mediante la API de presencia de Firebase.

En la línea de comando, ejecute los siguientes comandos para agregar las dependencias necesarias:

flutter pub add firebase_app_installations

flutter pub add firebase_database

crear una base de datos

En la consola de Firebase,

  1. Vaya a la sección Base de datos en tiempo real de la consola de Firebase . Haga clic en Crear base de datos .
  2. Si se le solicita que seleccione un modo de inicio para sus reglas de seguridad, seleccione Modo de prueba por ahora**.** (El modo de prueba crea reglas de seguridad que permiten el paso de todas las solicitudes. Agregará reglas de seguridad más adelante. Es importante nunca pasar a producción con sus reglas de seguridad aún en modo de prueba).

La base de datos está vacía por ahora. Ubique su databaseURL en Configuración del proyecto , en la pestaña General . Desplácese hacia abajo hasta la sección Aplicaciones web .

1b6076f60a36263b.png

Agregue su databaseURL al archivo firebase_options.dart :

lib/firebase_options.dart

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

Registre dispositivos utilizando la API de presencia de RTDB

Desea registrar los dispositivos de un usuario cuando aparecen en línea. Para hacer esto, aprovechará las instalaciones de Firebase y la API de presencia de Firebase RTDB para realizar un seguimiento de una lista de dispositivos en línea de un solo usuario. El siguiente código ayudará a lograr este objetivo:

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

Vuelva a la línea de comandos, cree y ejecute la aplicación en su dispositivo o en un navegador con flutter run.

En su aplicación, inicie sesión como usuario. Recuerda iniciar sesión como el mismo usuario en diferentes plataformas.

En la consola de Firebase , debería ver que sus dispositivos aparecen bajo una ID de usuario en su base de datos.

5bef49cea3564248.png

6. Estado del dispositivo de sincronización

Seleccione un dispositivo principal

Para sincronizar estados entre dispositivos, designe un dispositivo como líder o controlador. El dispositivo principal dictará los estados en los dispositivos seguidores.

Cree un método setLeadDevice en application_state.dart y rastree este dispositivo con la clave active_device en 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;
      });
    }
  }

Para agregar esta funcionalidad al menú contextual de la barra de la aplicación, cree un PopupMenuItem llamado Controller modificando el widget SignedInMenuButton . Este menú permitirá a los usuarios configurar el dispositivo principal.

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

Escribir el estado del dispositivo principal en la base de datos

Una vez que haya configurado un dispositivo principal, puede sincronizar los estados del dispositivo principal con RTDB con el siguiente código. Agregue el siguiente código al final de application_state.dart. Esto comenzará a almacenar dos atributos: el estado del reproductor (reproducir o pausar) y la posición del control deslizante.

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

Y, por último, debe llamar setActiveDeviceState cada vez que se actualice el estado del reproductor del controlador. Realice los siguientes cambios en el archivo player_widget.dart existente:

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

Lea el estado del dispositivo principal de la base de datos

Hay dos partes para leer y usar el estado del dispositivo principal. Primero, desea configurar un oyente de base de datos del estado del reproductor principal en application_state . Este oyente le dirá a los dispositivos seguidores cuándo actualizar la pantalla a través de una devolución de llamada. Observe que ha definido una interfaz OnLeadDeviceChangeCallback en este paso. Aún no está implementado; implementará esta interfaz en player_widget.dart en el próximo paso.

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

En segundo lugar, inicie el escucha de la base de datos durante la inicialización del reproductor en player_widget.dart . Pase la función _updatePlayer para que el estado del jugador seguidor pueda actualizarse siempre que cambie el valor de la base de datos.

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

Ahora estás listo para probar la aplicación:

  1. En la línea de comando, ejecute la aplicación en emuladores y/o en un navegador con: flutter run -d <device-name>
  2. Abra las aplicaciones en un navegador, en un simulador de iOS o en un emulador de Android. Vaya al menú contextual, elija una aplicación para que sea el dispositivo líder. Debería poder ver cómo cambian los reproductores de los dispositivos seguidores a medida que se actualiza el dispositivo líder.
  3. Ahora cambie el dispositivo líder, reproduzca o pause la música y observe cómo se actualizan los dispositivos seguidores en consecuencia.

Si los dispositivos seguidores se actualizan correctamente, ha logrado crear un controlador de dispositivos cruzados. Sólo queda un paso crucial.

7. Actualizar reglas de seguridad

¡A menos que escribamos mejores reglas de seguridad, alguien podría escribir un estado en un dispositivo que no es de su propiedad! Entonces, antes de terminar, actualice las Reglas de seguridad de la base de datos en tiempo real para asegurarse de que los únicos usuarios que pueden leer o escribir en un dispositivo sean los usuarios que iniciaron sesión en ese dispositivo. En Firebase Console, vaya a Realtime Database y luego a la pestaña Reglas . Pegue las siguientes reglas que permiten que solo los usuarios registrados lean y escriban sus propios estados de dispositivo:

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

8. ¡Felicitaciones!

bcd986f7106d892b.gif

¡Felicitaciones, ha creado con éxito un controlador remoto de dispositivos cruzados usando Flutter!

Créditos

Mejor juntos, una canción de Firebase

  • Música de Ryan Vernon
  • Letra y portada del álbum por Marissa Christy
  • Voz de JP Gómez

9. Bono

Como desafío adicional, considere usar Flutter FutureBuilder para agregar el tipo de dispositivo principal actual a la interfaz de usuario de forma asíncrona. Si necesita ayuda, se implementa en la carpeta que contiene el estado terminado del laboratorio de código.

Documentos de referencia y próximos pasos