Codelab de Firebase Multidispositivo

Codelab de Firebase Multidispositivo

Acerca de este codelab

subjectÚltima actualización: oct 31, 2022
account_circleEscrito por Yun Miao

1. Introducción

Última actualización: 14/03/2022

FlutterFire para la comunicación entre dispositivos

A medida que vemos que una gran cantidad de dispositivos de automatización del hogar, tecnología personal y wearables se conectan a Internet, la comunicación entre dispositivos se convierte en una parte cada vez más importante de la compilación de aplicaciones para dispositivos móviles. La configuración de la comunicación entre dispositivos, como controlar un navegador desde una app para teléfonos o controlar lo que se reproduce en la TV desde el teléfono, suele ser más compleja que crear una app para dispositivos móviles normal .

Firebase Realtime Database proporciona la API de Presence , que permite a los usuarios ver el estado en línea o sin conexión de su dispositivo. La usarás con el servicio de instalaciones de Firebase para hacer un seguimiento de todos los dispositivos en los que accedió el mismo usuario y conectarlos. Usarás Flutter para crear aplicaciones para varias plataformas con rapidez y, luego, compilarás un prototipo multidispositivo que reproduzca música en un dispositivo y controle la música en otro.

Qué compilarás

En este codelab, compilarás un control remoto simple para un reproductor de música. Tu app hará lo siguiente:

  • Tener un reproductor de música simple en Android, iOS y la Web compilado con Flutter
  • Permite que los usuarios accedan.
  • Conectar dispositivos cuando el mismo usuario accede a su cuenta en varios dispositivos
  • Permite que los usuarios controlen la reproducción de música en un dispositivo desde otro.

7f0279938e1d3ab5.gif

Qué aprenderás

  • Cómo compilar y ejecutar una app de reproductor de música de Flutter
  • Cómo permitir que los usuarios accedan con Firebase Authentication
  • Cómo usar la API de Presence de RTDB de Firebase y el servicio de instalaciones de Firebase para conectar dispositivos

Requisitos

  • Un entorno de desarrollo de Flutter Sigue 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 una posterior. Si tienes una versión anterior, ejecuta flutter upgrade..
  • Una cuenta de Firebase

2. Cómo prepararte

Obtén el código de partida

Creamos una app de reproductor de música en Flutter. El código de partida se encuentra en un repositorio de Git. Para comenzar, en la línea de comandos, clona el repositorio, muévete a la carpeta con el estado inicial y, luego, instala las dependencias:

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

cd cross-device-controller/starter_code

flutter pub get

Compila la app

Puedes trabajar con tu IDE favorito para compilar la app o usar la línea de comandos.

En el directorio de tu app, compila la app para la Web con el comando flutter run -d web-server.. Deberías poder ver el siguiente mensaje.

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

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

Si conoces el emulador de Android o el simulador de iOS, puedes compilar la app para esas plataformas y, luego, instalarla con el comando flutter run -d <device_name>.

La app web debería mostrar un reproductor de música independiente básico. Asegúrate de que las funciones del reproductor funcionen según lo previsto. Esta es una app de reproductor de música simple diseñada para este codelab. Solo puede reproducir una canción de Firebase, Better Together.

Configura un emulador de Android o un simulador de iOS

Si ya tienes un dispositivo Android o iOS para el desarrollo, puedes omitir este paso.

Para crear un emulador de Android, descarga Android Studio, que también admite el desarrollo de Flutter, y sigue las instrucciones que se indican en Cómo crear y administrar dispositivos virtuales.

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

3. Configuración de Firebase

Crea un proyecto de Firebase

Abre un navegador y ve a http://console.firebase.google.com/.

  1. Accede a Firebase.
  2. En Firebase console, haz clic en Agregar proyecto (o Crear un proyecto) y asígnale el nombre Firebase-Cross-Device-Codelab.
  3. Haz clic en las opciones de creación del proyecto. Si se te solicita, acepta las condiciones de Firebase. Omite la configuración de Google Analytics, ya que no utilizarás ese servicio en esta app.

No es necesario que descargues los archivos mencionados ni que cambies los archivos build.gradle. Los configurarás cuando inicialices FlutterFire.

Instala el SDK de Firebase

En la línea de comandos, en el directorio del proyecto, ejecuta el siguiente comando para instalar Firebase:

flutter pub add firebase_core

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

Cómo inicializar FlutterFire

  1. Si no tienes Firebase CLI instalado, puedes ejecutar curl -sL https://firebase.tools | bash para instalarlo.
  2. Ejecuta firebase login y sigue las instrucciones para acceder.
  3. Ejecuta dart pub global activate flutterfire_cli para instalar la CLI de FlutterFire.
  4. Ejecuta flutterfire configure para configurar la CLI de FlutterFire.
  5. En el mensaje, elige el proyecto que acabas de crear para este codelab, como Firebase-Cross-Device-Codelab.
  6. Selecciona iOS, Android y Web cuando se te solicite que elijas la compatibilidad de configuración.
  7. Cuando se te solicite el ID de paquete de Apple, escribe un dominio único o ingresa com.example.appname, que es adecuado para los fines de este codelab.

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

En el editor, agrega el siguiente código al 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());
}

Compila la app con el siguiente comando:

flutter run

Aún no cambiaste ningún elemento de la IU, por lo que el aspecto y el comportamiento de la app no cambiaron. Pero ahora tienes una app de Firebase y puedes comenzar a usar los productos de Firebase, incluidos los siguientes:

  • Firebase Authentication, que permite que los usuarios accedan a tu app
  • Firebase Realtime Database(RTDB): Usarás la API de Presence para hacer un seguimiento del estado en línea o sin conexión del dispositivo.
  • Las reglas de seguridad de Firebase te permitirán proteger la base de datos.
  • El Servicio de instalaciones de Firebase para identificar los dispositivos en los que accedió un solo usuario

4. Agrega Firebase Auth

Habilita el acceso con correo electrónico para Firebase Authentication

Para permitir que los usuarios accedan a la app web, usarás el método de acceso Correo electrónico/Contraseña:

  1. En Firebase console, expande el menú Build en el panel izquierdo.
  2. Haz clic en Authentication y, luego, en el botón Get Started y, luego, en la pestaña Sign-in method.
  3. Haz clic en Correo electrónico/contraseña en la lista Proveedores de acceso, establece el interruptor Habilitar en la posición de activado y, luego, haz clic en Guardar. 58e3e3e23c2f16a4.png

Cómo configurar Firebase Authentication en Flutter

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

flutter pub add firebase_auth

flutter pub add provider

Con esta configuración, ahora puedes crear el flujo de acceso y cierre de sesión. Dado que el estado de autenticación no debe cambiar de una pantalla a otra, crearás una clase application_state.dart para hacer un seguimiento de los cambios de estado a nivel de la app, como el acceso y la salida. Obtén más información sobre esto en la documentación de administración de estados de Flutter.

Pega 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 asegurarte de que ApplicationState se inicialice cuando se inicie la app, agregarás 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 IU de la aplicación debería haber permanecido igual, pero ahora puedes permitir que los usuarios accedan y guarden los estados de la app.

Crea un flujo de acceso

En este paso, trabajarás en el flujo de acceso y salida. El flujo se verá de la siguiente manera:

  1. Un usuario que salió de su cuenta iniciará el flujo de acceso haciendo clic en el menú contextual 71fcc1030a336423.png que se encuentra a la derecha de la barra de la aplicación.
  2. El flujo de acceso se mostrará en un diálogo.
  3. Si el usuario nunca accedió antes, se le pedirá que cree una cuenta con una dirección de correo electrónico y una contraseña válidas.
  4. Si el usuario ya accedió, se le pedirá que ingrese su contraseña.
  5. Una vez que el usuario haya accedido, si hace clic en el menú contextual, aparecerá la opción Salir.

c295f6fa2e1d40f3.png

Para agregar el flujo de acceso, debes seguir tres pasos.

En primer lugar, crea 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';

Agrega 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, crea 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, busca el widget de appBar existente en main.dart. Agrega el AppBarMenuButton para mostrar la opción Acceder o Salir.

lib/main.dart

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

Ejecuta el comando flutter run para reiniciar la app con estos cambios. Deberías ver el menú contextual 71fcc1030a336423.png en el lado derecho de la barra de la app. Si haces clic en él, se abrirá un diálogo de acceso.

Una vez que accedas con una dirección de correo electrónico y una contraseña válidas, deberías ver la opción Salir en el menú contextual.

En Firebase console, en Authentication, deberías poder ver la dirección de correo electrónico como un usuario nuevo.

888506c86a28a72c.png

¡Felicitaciones! Ahora los usuarios pueden acceder a la app.

5. Agrega una conexión a la base de datos

Ya puedes continuar con el registro de dispositivos con la API de Firebase Presence.

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

flutter pub add firebase_app_installations

flutter pub add firebase_database

Crea una base de datos

En Firebase console, haz lo siguiente:

  1. Ve a la sección Realtime Database de Firebase console. Haz clic en Crear base de datos.
  2. Si se te solicita que selecciones un modo de inicio para tus reglas de seguridad, selecciona Modo de prueba por ahora**.** (El modo de prueba crea reglas de seguridad que permiten todas las solicitudes. Agregarás reglas de seguridad más adelante. Es importante que nunca pases a producción con tus reglas de seguridad en modo de prueba).

Por ahora, la base de datos está vacía. Busca tu databaseURL en Configuración del proyecto, en la pestaña General. Desplázate hacia abajo hasta la sección Apps web.

1b6076f60a36263b.png

Agrega tu databaseURL al archivo firebase_options.dart:

lib/firebase_options.dart

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

Registra dispositivos con la API de RTDB Presence

Quieres registrar los dispositivos de un usuario cuando aparecen en línea. Para ello, aprovecharás Firebase Installations y la API de Firebase RTDB Presence para hacer un seguimiento de una lista de dispositivos en línea de un solo usuario. El siguiente código te 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';
 
}

En la línea de comandos, compila y ejecuta la app en tu dispositivo o en un navegador con flutter run..

En tu app, accede como usuario. Recuerda acceder con el mismo usuario en diferentes plataformas.

En Firebase console, deberías ver tus dispositivos en un ID de usuario en tu base de datos.

5bef49cea3564248.png

6. Sincroniza el estado del dispositivo

Selecciona un dispositivo de clientes potenciales

Para sincronizar los estados entre dispositivos, designa uno como el líder o el controlador. El dispositivo principal dictará los estados de los dispositivos secundarios.

Crea un método setLeadDevice en application_state.dart y haz un seguimiento de 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 app, crea 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();
    }
  }
}

Escribe el estado del dispositivo principal en la base de datos

Una vez que hayas configurado un dispositivo principal, puedes sincronizar sus estados con RTDB con el siguiente código. Agrega el siguiente código al final de application_state.dart.. Esto comenzará a almacenar dos atributos: el estado del reproductor (reproducción o pausa) 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');
      }
    }
  }

Por último, debes llamar a setActiveDeviceState cada vez que se actualice el estado del jugador del controlador. Realiza 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;
 
}

Cómo leer el estado del dispositivo principal desde la base de datos

Hay dos partes para leer y usar el estado del dispositivo principal. Primero, debes configurar un objeto de escucha de base de datos del estado del jugador principal en application_state. Este objeto de escucha les indicará a los dispositivos seguidores cuándo actualizar la pantalla a través de una devolución de llamada. Observa que definiste una interfaz OnLeadDeviceChangeCallback en este paso. Aún no está implementada. Implementarás 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, inicia el objeto de escucha de la base de datos durante la inicialización del reproductor en player_widget.dart. Pasa la función _updatePlayer para que se pueda actualizar el estado del jugador de seguidor 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');
        }
      }
    }
  }

Ya puedes probar la app:

  1. En la línea de comandos, ejecuta la app en emuladores o en un navegador con flutter run -d <device-name>.
  2. Abre las apps en un navegador, en un simulador de iOS o en un emulador de Android. Ve al menú contextual y elige una app para que sea el dispositivo líder. Deberías poder ver que los reproductores de los dispositivos seguidores cambian a medida que se actualiza el dispositivo líder.
  3. Ahora, cambia el dispositivo líder, reproduce o pausa música y observa cómo se actualizan los dispositivos seguidores según corresponda.

Si los dispositivos seguidores se actualizan correctamente, significa que creaste un controlador multidispositivo. Solo queda un paso fundamental.

7. Actualiza 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. Por lo tanto, antes de terminar, actualiza las reglas de seguridad de Realtime Database para asegurarte de que los únicos usuarios que puedan leer o escribir en un dispositivo sean los que accedan a él. En Firebase console, navega a Realtime Database y, luego, a la pestaña Rules. Pega las siguientes reglas para permitir que solo el usuario que accedió pueda leer y escribir sus propios estados del dispositivo:

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

8. ¡Felicitaciones!

bcd986f7106d892b.gif

¡Felicitaciones! Compilaste correctamente un control remoto multidispositivo con Flutter.

Créditos

Better Together, una canción de Firebase

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

9. Contenido adicional

Como desafío adicional, considera usar Flutter FutureBuilder para agregar el tipo de dispositivo principal actual a la IU de forma asíncrona. Si necesitas asistencia, esta se implementa en la carpeta que contiene el estado final del codelab.

Documentos de referencia y próximos pasos