Atelier de programmation Firebase Cross Device

1. Introduction

Dernière mise à jour : 14/03/2022

FlutterFire pour la communication multi-appareils

Avec l'arrivée en ligne d'un grand nombre d'appareils domotiques, de wearables et de technologies de santé personnelle, la communication inter-appareils devient un élément de plus en plus important dans la création d'applications mobiles. La configuration de la communication entre appareils, comme le contrôle d'un navigateur à partir d'une application mobile ou le contrôle de ce qui est lu sur votre téléviseur à partir de votre téléphone, est traditionnellement plus complexe que la création d'une application mobile normale .

Firebase Realtime Database fournit l'API Presence , qui permet aux utilisateurs de voir l'état en ligne/hors connexion de leur appareil. Vous l'utiliserez avec le service Firebase Installations pour suivre et connecter tous les appareils sur lesquels le même utilisateur s'est connecté. Vous utiliserez Flutter pour créer rapidement des applications pour plusieurs plates-formes, puis vous créerez un prototype multi-appareils qui lit de la musique sur un appareil et la contrôle sur un autre.

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez créer une télécommande simple pour un lecteur de musique. Cette application pourra :

  • Créez un lecteur de musique simple sur Android, iOS et le Web avec Flutter.
  • Autoriser les utilisateurs à se connecter.
  • Associez des appareils lorsque le même utilisateur est connecté sur plusieurs appareils.
  • Les applications de cette catégorie permettent aux utilisateurs de contrôler la lecture de musique sur un appareil à partir d'un autre appareil.

7f0279938e1d3ab5.gif

Points abordés

  • Créer et exécuter une application de lecteur de musique Flutter.
  • Découvrez comment autoriser les utilisateurs à se connecter avec Firebase Auth.
  • Découvrez comment utiliser l'API Presence Firebase RTDB et le service d'installation Firebase pour connecter des appareils.

Prérequis

  • Un environnement de développement Flutter. Suivez les instructions du guide d'installation de Flutter pour le configurer.
  • Une version minimale de Flutter 2.10 ou ultérieure est requise. Si vous disposez d'une version antérieure, exécutez flutter upgrade..
  • Un compte Firebase.

2. Configuration

Télécharger le code de démarrage

Nous avons créé une application de lecteur de musique dans Flutter. Le code de démarrage se trouve dans un dépôt Git. Pour commencer, clonez le dépôt sur la ligne de commande, accédez au dossier avec l'état de départ et installez les dépendances :

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

cd cross-device-controller/starter_code

flutter pub get

Compiler l'application

Vous pouvez utiliser votre IDE préféré pour créer l'application ou utiliser la ligne de commande.

Dans le répertoire de votre application, créez l'application pour le Web avec la commande flutter run -d web-server.. L'invite suivante devrait s'afficher.

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

Accédez à http://localhost:<port> pour afficher le lecteur de musique.

Si vous connaissez l'émulateur Android ou le simulateur iOS, vous pouvez créer l'application pour ces plates-formes et l'installer avec la commande flutter run -d <device_name>.

L'application Web doit afficher un lecteur de musique autonome de base. Assurez-vous que les fonctionnalités du lecteur fonctionnent comme prévu. Il s'agit d'une application de lecteur de musique simple conçue pour cet atelier de programmation. Il ne peut lire qu'un seul titre Firebase, Better Together.

Configurer un émulateur Android ou un simulateur iOS

Si vous disposez déjà d'un appareil Android ou iOS pour le développement, vous pouvez ignorer cette étape.

Pour créer un émulateur Android, téléchargez Android Studio, qui est également compatible avec le développement Flutter, puis suivez les instructions de Créer et gérer des appareils virtuels.

Pour créer un simulateur iOS, vous aurez besoin d'un environnement Mac. Téléchargez Xcode, puis suivez les instructions de Présentation du simulateur > Utiliser le simulateur > Ouvrir et fermer un simulateur.

3. Configurer Firebase

Créer un projet Firebase

  1. Connectez-vous à la console Firebase à l'aide de votre compte Google.
  2. Cliquez sur le bouton pour créer un projet, puis saisissez un nom de projet (par exemple, Firebase-Cross-Device-Codelab).
  3. Cliquez sur Continuer.
  4. Si vous y êtes invité, lisez et acceptez les Conditions d'utilisation de Firebase, puis cliquez sur Continuer.
  5. (Facultatif) Activez l'assistance IA dans la console Firebase (appelée "Gemini dans Firebase").
  6. Pour cet atelier de programmation, vous n'avez pas besoin de Google Analytics. Désactivez donc l'option Google Analytics.
  7. Cliquez sur Créer un projet, attendez que votre projet soit provisionné, puis cliquez sur Continuer.

Installer le SDK Firebase

De retour sur la ligne de commande, dans le répertoire du projet, exécutez la commande suivante pour installer Firebase :

flutter pub add firebase_core

Dans le fichier pubspec.yaml, définissez la version de firebase_core sur au moins 1.13.1 ou exécutez flutter upgrade.

Initialiser FlutterFire

  1. Si la CLI Firebase n'est pas installée, vous pouvez l'installer en exécutant curl -sL https://firebase.tools | bash.
  2. Connectez-vous en exécutant firebase login et en suivant les instructions.
  3. Installez la CLI FlutterFire en exécutant dart pub global activate flutterfire_cli.
  4. Configurez la CLI FlutterFire en exécutant flutterfire configure.
  5. À l'invite, choisissez le projet que vous venez de créer pour cet atelier de programmation, par exemple Firebase-Cross-Device-Codelab.
  6. Sélectionnez iOS, Android et Web lorsque vous êtes invité à choisir la compatibilité de la configuration.
  7. Lorsque vous êtes invité à saisir l'ID de bundle Apple, saisissez un domaine unique ou com.example.appname, ce qui convient pour cet atelier de programmation.

Une fois configuré, un fichier firebase_options.dart contenant toutes les options requises pour l'initialisation sera généré pour vous.

Dans votre éditeur, ajoutez le code suivant à votre fichier main.dart pour initialiser Flutter et 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());
}

Compilez l'application avec la commande suivante :

flutter run

Vous n'avez encore modifié aucun élément de l'UI. L'apparence et le comportement de l'application n'ont donc pas changé. Vous disposez désormais d'une application Firebase et vous pouvez commencer à utiliser les produits Firebase, y compris :

  • Firebase Authentication, qui permet à vos utilisateurs de se connecter à votre application.
  • Firebase Realtime Database(RTDB) : vous utiliserez l'API Presence pour suivre l'état en ligne/hors connexion des appareils.
  • Les règles de sécurité Firebase vous permettent de sécuriser la base de données.
  • Service Firebase Installations pour identifier les appareils sur lesquels un même utilisateur s'est connecté.

4. Ajouter Firebase Auth

Activer la connexion par e-mail pour Firebase Authentication

Pour permettre aux utilisateurs de se connecter à l'application Web, vous allez définir la méthode de connexion Email/Password (Adresse e-mail/Mot de passe) :

  1. Dans la console Firebase, développez le menu Créer dans le panneau de gauche.
  2. Cliquez sur Authentification, puis sur le bouton Commencer, puis sur l'onglet Méthode de connexion.
  3. Cliquez sur Adresse e-mail/Mot de passe dans la liste Fournisseurs de connexion, mettez le bouton bascule Activer en position activée, puis cliquez sur Enregistrer. 58e3e3e23c2f16a4.png

Configurer Firebase Authentication dans Flutter

Sur la ligne de commande, exécutez les commandes suivantes pour installer les packages Flutter nécessaires :

flutter pub add firebase_auth

flutter pub add provider

Avec cette configuration, vous pouvez maintenant créer le flux de connexion et de déconnexion. Étant donné que l'état d'authentification ne doit pas changer d'un écran à l'autre, vous allez créer une classe application_state.dart pour suivre les changements d'état au niveau de l'application, comme la connexion et la déconnexion. Pour en savoir plus, consultez la documentation Gestion de l'état Flutter.

Collez ce qui suit dans le nouveau fichier 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();
  }
}

Pour vous assurer que ApplicationState sera initialisé au démarrage de l'application, vous devez ajouter une étape d'initialisation à 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(),
  ));
}

Encore une fois, l'interface utilisateur de l'application devrait être restée la même, mais vous pouvez désormais permettre aux utilisateurs de se connecter et d'enregistrer les états de l'application.

Créer un flux de connexion

Dans cette étape, vous allez travailler sur le flux de connexion et de déconnexion. Voici à quoi ressemblera le flux :

  1. Un utilisateur déconnecté lancera le flux de connexion en cliquant sur le menu contextuel 71fcc1030a336423.png sur le côté droit de la barre d'application.
  2. Le processus de connexion s'affiche dans une boîte de dialogue.
  3. Si l'utilisateur ne s'est jamais connecté auparavant, il sera invité à créer un compte à l'aide d'une adresse e-mail et d'un mot de passe valides.
  4. Si l'utilisateur s'est déjà connecté, il sera invité à saisir son mot de passe.
  5. Une fois l'utilisateur connecté, l'option Se déconnecter s'affiche lorsqu'il clique sur le menu contextuel.

c295f6fa2e1d40f3.png

L'ajout d'un flux de connexion nécessite trois étapes.

Commencez par créer un widget AppBarMenuButton. Ce widget contrôle le pop-up du menu contextuel en fonction du loginState de l'utilisateur. Ajouter les importations

lib/src/widgets.dart

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

Ajoutez le code suivant à 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),
        ),
      ),
    ];
  }
}

Ensuite, dans la même classe widgets.dart, créez le 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,
          ),
        ),
      ]),
    );
  }
}

Troisièmement, recherchez le widget appBar existant dans main.dart.. Ajoutez AppBarMenuButton pour afficher l'option Se connecter ou Se déconnecter.

lib/main.dart

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

Exécutez la commande flutter run pour redémarrer l'application avec ces modifications. Le menu contextuel 71fcc1030a336423.png devrait s'afficher sur le côté droit de la barre d'application. Si vous cliquez dessus, une boîte de dialogue de connexion s'affiche.

Une fois que vous vous êtes connecté avec une adresse e-mail et un mot de passe valides, l'option Se déconnecter devrait s'afficher dans le menu contextuel.

Dans la console Firebase, sous Authentication (Authentification), vous devriez voir l'adresse e-mail listée en tant que nouvel utilisateur.

888506c86a28a72c.png

Félicitations ! Les utilisateurs peuvent désormais se connecter à l'application.

5. Ajouter une connexion à une base de données

Vous êtes maintenant prêt à passer à l'enregistrement de l'appareil à l'aide de l'API Firebase Presence.

Dans la ligne de commande, exécutez les commandes suivantes pour ajouter les dépendances nécessaires :

flutter pub add firebase_app_installations

flutter pub add firebase_database

Créer une base de données

Dans la console Firebase,

  1. Accédez à la section Realtime Database de la console Firebase. Cliquez sur Créer une base de données.
  2. Si vous êtes invité à sélectionner un mode de démarrage pour vos règles de sécurité, sélectionnez Mode test pour le moment**.** (Le mode test crée des règles de sécurité qui autorisent toutes les requêtes. Vous ajouterez des règles de sécurité ultérieurement. Il est important de ne jamais passer en production tant que vos règles de sécurité sont encore en mode test.)

La base de données est vide pour le moment. Recherchez votre databaseURL dans Paramètres du projet, sous l'onglet Général. Faites défiler la page jusqu'à la section Applications Web.

1b6076f60a36263b.png

Ajoutez votre databaseURL au fichier firebase_options.dart.:

lib/firebase_options.dart

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

Enregistrer des appareils à l'aide de l'API Presence RTDB

Vous souhaitez enregistrer les appareils d'un utilisateur lorsqu'ils apparaissent en ligne. Pour ce faire, vous allez tirer parti de Firebase Installations et de l'API Firebase RTDB Presence pour suivre la liste des appareils en ligne d'un même utilisateur. Le code suivant vous aidera à atteindre cet objectif :

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 retour sur la ligne de commande, compilez et exécutez l'application sur votre appareil ou dans un navigateur avec flutter run..

Dans votre application, connectez-vous en tant qu'utilisateur. N'oubliez pas de vous connecter avec le même compte sur les différentes plates-formes.

Dans la console Firebase, vos appareils devraient s'afficher sous un seul ID utilisateur dans votre base de données.

5bef49cea3564248.png

6. Synchroniser l'état de l'appareil

Sélectionnez un appareil principal

Pour synchroniser les états entre les appareils, désignez-en un comme appareil principal ou contrôleur. L'appareil principal détermine l'état des appareils secondaires.

Créez une méthode setLeadDevice dans application_state.dart et suivez cet appareil avec la clé active_device dans 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;
      });
    }
  }

Pour ajouter cette fonctionnalité au menu contextuel de la barre d'application, créez un PopupMenuItem appelé Controller en modifiant le widget SignedInMenuButton. Ce menu permettra aux utilisateurs de définir l'appareil 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();
    }
  }
}

Écrire l'état de l'appareil principal dans la base de données

Une fois que vous avez défini un appareil principal, vous pouvez synchroniser ses états avec RTDB à l'aide du code suivant. Ajoutez le code suivant à la fin de application_state.dart.. Cela commencera à stocker deux attributs : l'état du lecteur (lecture ou pause) et la position du curseur.

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

Enfin, vous devez appeler setActiveDeviceState chaque fois que l'état du lecteur du contrôleur est mis à jour. Apportez les modifications suivantes au fichier player_widget.dart existant :

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

Lire l'état de l'appareil principal à partir de la base de données

Pour lire et utiliser l'état de l'appareil principal, deux parties sont nécessaires. Tout d'abord, vous devez configurer un écouteur de base de données de l'état du lecteur principal dans application_state. Cet écouteur indiquera aux appareils suiveurs quand mettre à jour l'écran via un rappel. Notez que vous avez défini une interface OnLeadDeviceChangeCallback à cette étape. Elle n'est pas encore implémentée. Vous implémenterez cette interface dans player_widget.dart à l'étape suivante.

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

Ensuite, démarrez l'écouteur de base de données lors de l'initialisation du lecteur dans player_widget.dart. Transmettez la fonction _updatePlayer afin que l'état du lecteur suiveur puisse être mis à jour chaque fois que la valeur de la base de données change.

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

Vous êtes maintenant prêt à tester l'application :

  1. Sur la ligne de commande, exécutez l'application sur des émulateurs et/ou dans un navigateur avec : flutter run -d <device-name>
  2. Ouvrez les applications dans un navigateur, sur un simulateur iOS ou sur un émulateur Android. Accédez au menu contextuel et choisissez une application comme appareil principal. Vous devriez voir les lecteurs des appareils suiveurs changer à mesure que l'appareil principal se met à jour.
  3. Modifiez maintenant l'appareil principal, lancez ou mettez en pause la musique, et observez la mise à jour des appareils secondaires en conséquence.

Si les appareils suiveurs se mettent à jour correctement, vous avez réussi à créer un contrôleur multi-appareils. Il ne reste plus qu'une étape essentielle.

7. Mettre à jour les règles de sécurité

Si nous n'écrivons pas de meilleures règles de sécurité, quelqu'un pourrait écrire un état sur un appareil qui ne lui appartient pas. Avant de terminer, mettez à jour les règles de sécurité de la base de données en temps réel pour vous assurer que seul l'utilisateur connecté à un appareil peut lire ou écrire des données sur cet appareil. Dans la console Firebase, accédez à Realtime Database, puis à l'onglet Règles. Collez les règles suivantes pour autoriser uniquement les utilisateurs connectés à lire et à écrire les états de leurs propres appareils :

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

8. Félicitations !

bcd986f7106d892b.gif

Félicitations, vous avez réussi à créer une télécommande multi-appareils à l'aide de Flutter !

Crédits

Better Together, une chanson Firebase

  • Musique de Ryan Vernon
  • Paroles et pochette de l'album par Marissa Christy
  • Voix de JP Gomez

9. Bonus

Pour un défi supplémentaire, envisagez d'utiliser Flutter FutureBuilder pour ajouter le type d'appareil principal actuel à l'UI de manière asynchrone. Si vous avez besoin d'aide, elle est implémentée dans le dossier contenant l'état final de l'atelier de programmation.

Documents de référence et étapes suivantes