Codelab de Firebase Multidispositivo

1. Introducción

Última actualización: 14/03/2022

FlutterFire para la comunicación entre dispositivos

A medida que somos testigos de que una gran cantidad de dispositivos de automatización del hogar y de dispositivos tecnológicos de salud personales y wearable se conectan en línea, la comunicación entre dispositivos se convierte en una parte cada vez más importante de la creación de aplicaciones 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 compilar una app para dispositivos móviles normal.

Realtime Database de Firebase proporciona la API de Presence , que permite a los usuarios ver el estado en línea o sin conexión de sus dispositivos. lo utilizarás con el servicio de instalaciones de Firebase para hacer un seguimiento de todos los dispositivos en los que haya accedido un 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 reproducir música. Tu app hará lo siguiente:

  • Tener un reproductor de música simple en Android, iOS y la Web compilado con Flutter
  • Permitir que los usuarios accedan.
  • Conectar dispositivos cuando el mismo usuario accede a su cuenta en varios dispositivos
  • Permiten 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 reproducción de música de Flutter
  • Cómo permitir que los usuarios accedan con Firebase Authentication
  • Cómo usar la API de Firebase RTDB Presence y el Servicio de instalación 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 de 2.10 o 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 reproducción 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 e 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 reproducción de música simple diseñada para este codelab. Solo se 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 usarás Analytics 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.

Inicializa 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. Cuando se te solicite, elige el proyecto que acabas de crear para este codelab, por ejemplo, Firebase-Cross-Device-Codelab.
  6. Selecciona iOS, Android y Web cuando se te solicite elegir la compatibilidad con la configuración.
  7. Cuando se te solicite el ID del paquete de Apple, escribe un dominio único o ingresa com.example.appname, lo cual está bien para este codelab.

Una vez configurado, se generará un archivo firebase_options.dart para ti 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 Presencia 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, puedes crear el flujo de acceso y salida. 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 80fcc1030a336423.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ó, 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, se requieren 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 poder ver el menú contextual 71fcc1030a336423.png en el lado derecho de la barra de la app. Si haces clic en ella, aparecerá 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 poder 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! Los usuarios ya pueden acceder a la app.

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

Ya está todo listo para 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 las reglas de seguridad, selecciona Modo de prueba por ahora**.** (El modo de prueba crea reglas de seguridad que permiten el ingreso de 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 el momento, 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 cliente potencial

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, modifica el widget SignedInMenuButton para crear un PopupMenuItem llamado Controller. 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 la 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 reproductor principal en application_state. Este objeto de escucha les indica a los dispositivos seguidores cuándo deben actualizar la pantalla mediante una devolución de llamada. Ten en cuenta que definiste una interfaz OnLeadDeviceChangeCallback en este paso. Aún no está implementado. 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();
      });
    }
  }

Segundo, 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 el estado del jugador seguidor se pueda actualizar 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 los dispositivos seguidos se actualizan según corresponda.

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

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 es de su propiedad. Por lo tanto, antes de terminar, actualiza las reglas de seguridad de Realtime Database para asegurarte de que los únicos usuarios que pueden realizar operaciones de lectura o escritura en un dispositivo sea el que accedió a él. En Firebase console, navega a Realtime Database y, luego, a la pestaña Reglas. 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 con éxito 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