Atelier de programmation multi-appareils Firebase

1. Introduction

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

FlutterFire pour la communication entre appareils

Alors que nous assistons à la mise en ligne d’un grand nombre d’appareils domotiques, portables et de technologie de santé personnelle, la communication entre appareils devient un élément de plus en plus important dans la création d’applications mobiles. La configuration d'une communication entre appareils, telle que le contrôle d'un navigateur à partir d'une application téléphonique ou le contrôle de ce qui est diffusé sur votre téléviseur à partir de votre téléphone, est traditionnellement plus complexe que la création d'une application mobile normale.

La base de données en temps réel de Firebase fournit l' API Presence qui permet aux utilisateurs de voir l'état en ligne/hors ligne de leur appareil ; vous l'utiliserez avec le service d'installation Firebase 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 joue musique sur un appareil et contrôle la musique sur un autre !

Ce que vous construirez

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

  • Disposez d'un simple lecteur de musique sur Android, iOS et Web, construit avec Flutter.
  • Autoriser les utilisateurs à se connecter.
  • Connectez des appareils lorsque le même utilisateur est connecté sur plusieurs appareils.
  • Autoriser les utilisateurs à contrôler la lecture de musique sur un appareil à partir d'un autre appareil.

7f0279938e1d3ab5.gif

Ce que vous apprendrez

  • Comment créer et exécuter une application de lecteur de musique Flutter.
  • Comment autoriser les utilisateurs à se connecter avec Firebase Auth.
  • Comment utiliser l'API de présence Firebase RTDB et le service d'installation Firebase pour connecter des appareils.

Ce dont vous aurez besoin

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

2. Mise en place

Obtenez 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, sur la ligne de commande, clonez le dépôt, déplacez-vous dans le 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

Créer l'application

Vous pouvez travailler avec 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. Vous devriez pouvoir voir l’invite suivante.

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

Visitez http://localhost:<port> pour voir le lecteur de musique.

Si vous êtes familier avec 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 jouer qu'une chanson 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 prend également en charge le développement Flutter, et suivez les instructions dans 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 et suivez les instructions dans Présentation du simulateur > Utiliser le simulateur > Ouvrir et fermer un simulateur .

3. Configuration de Firebase

Créer un projet Firebase

Ouvrez un navigateur sur http://console.firebase.google.com/ .

  1. Connectez-vous à Firebase .
  2. Dans la console Firebase, cliquez sur Ajouter un projet (ou Créer un projet ) et nommez votre projet Firebase Firebase-Cross-Device-Codelab .
  3. Cliquez sur les options de création de projet. Acceptez les conditions de Firebase si vous y êtes invité. Ignorez la configuration de Google Analytics, car vous n'utiliserez pas Analytics pour cette application.

Vous n'avez pas besoin de télécharger les fichiers mentionnés ou de modifier les fichiers build.gradle. Vous les configurerez lors de l'initialisation de FlutterFire.

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 , modifiez la version de firebase_core pour qu'elle soit 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 invites.
  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 prise en charge de la configuration.
  7. Lorsque vous êtes invité à saisir l' ID du bundle Apple , saisissez un domaine unique ou saisissez com.example.appname , ce qui convient aux besoins de cet atelier de programmation.

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

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 :

flutter run

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

  • Authentification Firebase , qui permet à vos utilisateurs de se connecter à votre application.
  • Base de données en temps réel Firebase (RTDB) ; vous utiliserez l'API de présence pour suivre l'état en ligne/hors ligne de l'appareil
  • Les règles de sécurité Firebase vous permettront de sécuriser la base de données.
  • Service d'installation Firebase pour identifier les appareils auxquels un seul utilisateur s'est connecté.

4. Ajouter l'authentification Firebase

Activer la connexion par e-mail pour l'authentification Firebase

Pour permettre aux utilisateurs de se connecter à l'application Web, vous utiliserez la méthode de connexion par 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 cliquez sur le bouton Commencer , puis sur l'onglet Méthode de connexion .
  3. Cliquez sur E-mail/Mot de passe dans la liste Fournisseurs de connexion , placez le commutateur Activer sur la position activé, puis cliquez sur Enregistrer . 58e3e3e23c2f16a4.png

Configurer l'authentification Firebase 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 désormais 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, tels que la connexion et la déconnexion. Apprenez-en plus à ce sujet dans la documentation sur la 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 ajouterez 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 aurait dû rester 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 travaillerez 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’applications.
  2. Le flux de connexion sera affiché dans une boîte de dialogue.
  3. Si l'utilisateur ne s'est jamais connecté auparavant, il sera invité à créer un compte en utilisant une adresse e-mail valide et un mot de passe.
  4. Si l'utilisateur s'est déjà connecté, il sera invité à saisir son mot de passe.
  5. Une fois l'utilisateur connecté, cliquer sur le menu contextuel affichera une option de déconnexion .

c295f6fa2e1d40f3.png

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

Tout d’abord, créez un widget AppBarMenuButton . Ce widget contrôlera le menu contextuel en fonction du loginState d'un 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),
        ),
      ),
    ];
  }
}

Deuxièmement, 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 le AppBarMenuButton pour afficher l’option de connexion ou de déconnexion .

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. Vous devriez pouvoir voir le menu contextuel 71fcc1030a336423.png sur le côté droit de la barre d’applications. En cliquant dessus, vous accéderez à une boîte de dialogue de connexion.

Une fois connecté avec une adresse e-mail valide et un mot de passe, vous devriez pouvoir voir une option de déconnexion dans le menu contextuel.

Dans la console Firebase, sous Authentication , vous devriez pouvoir voir l'adresse e-mail répertoriée en tant que nouvel utilisateur.

888506c86a28a72c.png

Toutes nos félicitations! Les utilisateurs peuvent désormais se connecter à l'application !

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

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

Sur 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 Base de données en temps réel 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 le mode Test pour l'instant**.** (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é plus tard. Il est important de ne jamais passer en production avec vos règles de sécurité toujours en mode test.)

La base de données est vide pour l'instant. Localisez l'URL de votre databaseURL dans les paramètres du projet , sous l'onglet Général . Faites défiler 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>,
    ...
  );

Enregistrez les appareils à l'aide de l'API de présence RTDB

Vous souhaitez enregistrer les appareils d'un utilisateur lorsqu'ils apparaissent en ligne. Pour ce faire, vous profiterez des installations Firebase et de l'API de présence Firebase RTDB pour suivre une liste d'appareils en ligne d'un seul 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, créez 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 en tant que même utilisateur sur différentes plateformes.

Dans la console Firebase , vous devriez voir vos appareils apparaître 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 un appareil comme leader ou contrôleur. L'appareil principal dictera les états sur les appareils suiveurs.

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 le périphérique 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 du périphérique principal dans la base de données

Une fois que vous avez défini un périphérique principal, vous pouvez synchroniser les états du périphérique principal avec RTDB avec le 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');
      }
    }
  }

Et 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 du périphérique principal à partir de la base de données

Il y a deux parties pour lire et utiliser l'état du périphérique principal. Tout d’abord, vous souhaitez configurer un écouteur de base de données de l’état du joueur principal dans application_state . Cet auditeur indiquera aux appareils suiveurs quand mettre à jour l'écran via un rappel. Notez que vous avez défini une interface OnLeadDeviceChangeCallback dans cette étape. Ce n'est pas encore mis en œuvre ; 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();
      });
    }
  }

Deuxièmement, 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 joueur 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 un émulateur Android. Accédez au menu contextuel, choisissez une application comme appareil leader. Vous devriez pouvoir voir les joueurs des appareils suiveurs changer à mesure que l'appareil leader est mis à jour.
  3. Changez maintenant l'appareil leader, écoutez ou mettez en pause la musique et observez la mise à jour des appareils suiveurs 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 cruciale.

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

À moins que nous n'écrivions de meilleures règles de sécurité, quelqu'un pourrait écrire un état sur un appareil qui ne lui appartient pas ! Donc, 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 les seuls utilisateurs pouvant lire ou écrire sur un appareil sont ceux qui sont connectés à cet appareil. Dans la console Firebase, accédez à la base de données en temps réel, puis à l'onglet Règles . Collez les règles suivantes autorisant uniquement les utilisateurs connectés à lire et à écrire les états de leur propre appareil :

{
  "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

Mieux ensemble, une chanson de Firebase

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

9. Bonus

Comme défi supplémentaire, envisagez d'utiliser Flutter FutureBuilder pour ajouter le type de périphérique principal actuel à l'interface utilisateur de manière asynchrone. Si vous avez besoin d'une assistance, elle est implémentée dans le dossier contenant l'état terminé de l'atelier de programmation.

Documents de référence et prochaines étapes