Laboratorio de código entre dispositivos de Firebase

1. Introducción

Última actualización: 2022-03-14

FlutterFire para comunicación entre dispositivos

A medida que somos testigos de la puesta en línea de una gran cantidad de dispositivos de tecnología de salud personal, portátiles y de automatización del hogar, 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 de su dispositivo en línea/fuera de línea; lo usará con el servicio de instalaciones de Firebase para rastrear y conectar todos los dispositivos en los que el mismo usuario inició sesión. Usará Flutter para crear rápidamente aplicaciones para múltiples plataformas y luego creará un prototipo multidispositivo que reproduzca música en un dispositivo y controla la música en otro!

lo que construirás

En este codelab, crearás un control remoto sencillo para un reproductor de música. Su aplicación:

  • Tenga un reproductor de música sencillo 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.
  • Permitir a los usuarios controlar 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 utilizar la API de presencia de Firebase RTDB y el servicio de instalación de Firebase para conectar dispositivos.

Lo que necesitarás

  • Un entorno de desarrollo de Flutter. Siga las instrucciones de la guía de instalación de Flutter para configurarlo.
  • Se requiere una versión mínima de Flutter 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, vaya 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

Construye la aplicación

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

En el directorio de su aplicación, cree 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 debería mostrar un reproductor de música independiente básico. Asegúrese de que las funciones del reproductor funcionen según lo previsto. Esta es una aplicación de reproductor de música sencilla diseñada para este codelab. 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 iOS para desarrollo, puede omitir este paso.

Para crear un emulador de Android, descargue Android Studio , que también admite 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. Configurar Firebase

Crear un proyecto de Firebase

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

  1. Inicia sesión en Firebase .
  2. En Firebase console, 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, ya que no utilizará Analytics para esta aplicación.

No es necesario descargar los archivos mencionados ni cambiar los archivos build.gradle. Los configurará cuando inicialice FlutterFire.

Instalar el SDK de Firebase

De vuelta en la línea de comando, 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 instalado Firebase CLI, puede instalarlo ejecutando curl -sL https://firebase.tools | bash .
  2. Inicie sesión ejecutando firebase login y siguiendo las indicaciones.
  3. Instale la CLI de FlutterFire 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 la compatibilidad con la 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 los fines de 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

Aún no has cambiado ningún elemento de la interfaz de usuario, por lo que la apariencia y el comportamiento de la aplicación no han cambiado. Pero ahora tienes una aplicación de Firebase y puedes comenzar a usar los productos de Firebase, que incluyen:

  • Autenticación de Firebase , 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.
  • Servicio de instalaciones de Firebase para identificar los dispositivos en los que un único usuario ha iniciado sesión.

4. Agregar autenticación de Firebase

Habilite 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 por correo electrónico/contraseña :

  1. En Firebase console, expanda el menú Construir en el panel izquierdo.
  2. Haga clic en Autenticación y luego 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 activado y luego haga clic en Guardar . 58e3e3e23c2f16a4.png

Configurar la autenticación de Firebase en Flutter

En la línea de comando, ejecute los siguientes comandos para instalar los paquetes de 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 gestión del 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(),
  ));
}

Nuevamente, la interfaz de usuario de la aplicación debería haber seguido siendo la misma, 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 aplicaciones.
  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 antes, 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á el menú contextual emergente dependiendo del loginState del usuario. Agregar 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ías poder ver el menú contextual. 71fcc1030a336423.png en el lado derecho de la barra de aplicaciones. 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 Firebase console, en Autenticación , debería poder ver la dirección de correo electrónico que figura como nuevo usuario.

888506c86a28a72c.png

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

5. Agregar conexión a la base de datos

Ahora está listo para pasar al registro del dispositivo utilizando 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. Navegue a la sección Base de datos en tiempo real de Firebase console . 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 están en modo de prueba).

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

1b6076f60a36263b.png

Agregue la URL de 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 RTDB

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

De vuelta en la línea de comando, 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 Firebase console , deberías ver tus dispositivos aparecer con una ID de usuario en tu base de datos.

5bef49cea3564248.png

6. Sincronizar el estado del dispositivo

Seleccione un dispositivo principal

Para sincronizar estados entre dispositivos, designe un dispositivo como líder o controlador. El dispositivo principal dictará los estados de 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 aplicaciones, 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();
    }
  }
}

Escriba 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 finalmente, debes 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;
  }

Leer el estado del dispositivo principal desde la base de datos

Hay dos partes para leer y utilizar el estado del dispositivo principal. Primero, desea configurar un detector de base de datos del estado del reproductor principal en application_state . Este oyente indicará a los dispositivos seguidores cuándo actualizar la pantalla mediante 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 siguiente 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 detector 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 cada vez 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 los jugadores de los dispositivos seguidores cambiar a medida que se actualiza el dispositivo líder.
  3. Ahora cambie el dispositivo líder, reproduzca o ponga en pausa la música y observe cómo los dispositivos seguidores se actualizan en consecuencia.

Si los dispositivos seguidores se actualizan correctamente, habrá logrado crear un controlador entre dispositivos. Sólo queda un paso crucial.

7. Actualizar las reglas de seguridad

A menos que escribamos mejores reglas de seguridad, ¡alguien podría escribir un estado en un dispositivo que no le pertenece! 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 puedan leer o escribir en un dispositivo sean los usuarios que hayan iniciado sesión en ese dispositivo. En Firebase Console, navegue hasta Realtime Database y luego a la pestaña Reglas . Pegue las siguientes reglas que permiten que solo los usuarios que hayan iniciado sesión lean y escriban los estados de su propio dispositivo:

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

8. ¡Felicitaciones!

bcd986f7106d892b.gif

¡Felicitaciones, ha creado con éxito un control remoto para múltiples dispositivos usando Flutter!

Créditos

Mejor juntos, una canción de Firebase

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

9. Bonificación

Como desafío adicional, considere usar Flutter FutureBuilder para agregar el tipo de dispositivo principal actual a la interfaz de usuario de forma asincrónica. Si necesita ayuda, se implementa en la carpeta que contiene el estado finalizado del codelab.

Documentos de referencia y próximos pasos