Atelier de programmation Firebase Cross Device

1. Introduction

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

FlutterFire pour la communication entre appareils

Alors que nous assistons à la mise en ligne d'un grand nombre d'appareils domotiques, d'accessoires connectés et de technologies de santé personnelles, la communication inter-appareil devient de plus en plus importante dans le développement d'applications mobiles. Il est généralement plus complexe de mettre en place une communication inter-appareil, telle que contrôler un navigateur depuis une application pour téléphone ou ce qui est diffusé sur votre téléviseur depuis votre téléphone .

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 d'installations Firebase pour suivre et connecter tous les appareils sur lesquels le même utilisateur s'est connecté. Vous utiliserez Flutter afin de créer rapidement des applications pour plusieurs plates-formes, puis vous développerez un prototype multi-appareil qui diffusera de la musique sur un appareil et la contrôlera sur un autre.

Objectifs de l'atelier

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

  • disposer d'un lecteur de musique simple sur Android, iOS et le Web, créé avec Flutter ;
  • Autorisez les utilisateurs à se connecter.
  • Connexion des appareils lorsque le même utilisateur est connecté sur plusieurs appareils.
  • Autorisez les utilisateurs à contrôler la lecture de musique sur un appareil depuis un autre appareil.

7f0279938e1d3ab5.gif

Points abordés

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

Prérequis

  • Un environnement de développement Flutter Pour le configurer, suivez les instructions du guide d'installation de Flutter.
  • Vous devez disposer de Flutter 2.10 ou version ultérieure. 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 lecture de musique dans Flutter. Le code de démarrage se trouve dans un dépôt Git. Pour commencer, clonez le dépôt dans la ligne de commande, accédez au dossier contenant 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 utiliser votre IDE préféré pour créer l'application ou la ligne de commande.

Dans le répertoire de votre application, créez-la pour le Web à l'aide de la commande flutter run -d web-server.. Vous devriez voir l'invite suivante.

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 à l'aide de 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'une chanson Firebase, Better Together.

Configurer un émulateur Android ou un simulateur iOS

Si vous disposez déjà d'un appareil Android ou iOS à des fins de 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, et suivez les instructions de la section Créer et gérer des appareils virtuels.

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

3. Configurer Firebase

Créer un projet Firebase

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

  1. Connectez-vous à Firebase.
  2. Dans la console Firebase, cliquez sur Ajouter un projet (ou Créer un projet), puis nommez votre projet Firebase Firebase-Cross-Device-Codelab.
  3. Cliquez sur les options souhaitées. Si vous y êtes invité, acceptez les conditions d'utilisation de Firebase. 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 ni de modifier les fichiers build.gradle. Vous les configurerez lorsque vous initialiserez FlutterFire.

Installer le SDK Firebase

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, sélectionnez 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 du bundle Apple, saisissez un domaine unique ou saisissez com.example.appname (ce qui convient à cet atelier de programmation).

Une fois la configuration effectuée, un fichier firebase_options.dart contenant toutes les options requises pour l'initialisation sera généré automatiquement.

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 pas encore modifié d'éléments d'interface utilisateur. 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 de présence pour suivre l'état en ligne/hors connexion des appareils.
  • 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 sur lesquels un utilisateur unique 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 Adresse e-mail/Mot de passe :

  1. Dans la console Firebase, développez le menu Build (Compiler) dans le panneau de gauche.
  2. Cliquez sur Authentification, puis sur le bouton Commencer et enfin 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

Dans 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

Cette configuration vous permet désormais de 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. Pour en savoir plus, consultez 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 est initialisé au démarrage de l'application, vous allez 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 autoriser les utilisateurs à se connecter et à enregistrer les états de l'application.

Créer un flux de connexion

Au cours de cette étape, vous allez suivre le processus de connexion et de déconnexion. Voici à quoi ressemblera le flux:

  1. Un utilisateur déconnecté lance le flux de connexion en cliquant sur le menu contextuel 71fcc1030a336423.png à droite de la barre d'application.
  2. Le flux de connexion s'affiche dans une boîte de dialogue.
  3. Si l&#39;utilisateur ne s&#39;est jamais connecté auparavant, il est invité à créer un compte avec une adresse e-mail et un mot de passe valides.
  4. S'il s'est déjà connecté auparavant, il sera invité à saisir son mot de passe.
  5. Une fois l'utilisateur connecté, l'option Déconnexion s'affiche lorsque l'utilisateur clique sur le menu contextuel.

c295f6fa2e1d40f3.png

L&#39;ajout d&#39;un flux de connexion se déroule en trois étapes.

Commencez par créer un widget AppBarMenuButton. Ce widget contrôle le menu contextuel pop-up en fonction de l'loginState de l'utilisateur. Ajoutez 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 le 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 doit s'afficher à droite de la barre d'application. Si vous cliquez dessus, une boîte de dialogue de connexion s'ouvre.

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

Dans la console Firebase, sous 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 à la base de données

Vous êtes maintenant prêt à enregistrer des appareils à 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 (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 Test Mode (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é en mode test.)

La base de données est vide pour le moment. Localisez 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 RTDB Presence

Vous souhaitez enregistrer les appareils d&#39;un utilisateur lorsqu&#39;ils apparaissent en ligne. Pour ce faire, vous utiliserez les installations Firebase et l'API Firebase RTDB Presence pour suivre la liste des appareils en ligne d'un seul utilisateur. Le code suivant permet d'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';
  }

Revenez à 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 nom d'utilisateur sur différentes plates-formes.

Dans la console Firebase, vos appareils devraient apparaître sous un seul ID utilisateur dans votre base de données.

5bef49cea3564248.png

6. Synchroniser l'état de l'appareil

Sélectionner un appareil pour prospects

Pour synchroniser les états entre les appareils, désignez un appareil comme responsable ou contrôleur. L'appareil principal détermine les états des 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 une PopupMenuItem appelée 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.. Deux attributs seront alors stockés : 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 joueur 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

Deux parties permettent de lire et d'utiliser l'état de l'appareil principal. Tout d'abord, vous devez configurer un écouteur de base de données de l'état du joueur principal dans application_state. Cet écouteur indiquera aux appareils abonnés à quel moment 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 allez implémenter 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 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. Dans 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, dans un simulateur iOS ou dans un émulateur Android. Dans le menu contextuel, choisissez une application comme appareil principal. Vous devriez voir les joueurs des appareils suiveurs changer à mesure que l'appareil leader est mis à jour.
  3. Modifiez maintenant l'appareil maître, lancez ou mettez en pause la musique, et observez les appareils suiveurs se mettre à jour en conséquence.

Si les appareils abonnés se mettent à jour correctement, cela signifie que vous avez réussi à créer un contrôleur inter-appareil. Il ne reste qu'une étape cruciale.

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é Realtime Database pour vous assurer que seuls les utilisateurs connectés à un appareil peuvent lire ou écrire des données sur un appareil. Dans la console Firebase, accédez à Realtime Database, puis à l'onglet Règles. Collez les règles suivantes, qui n'autorisent qu'un utilisateur connecté à lire et à écrire ses propres états d'appareil :

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

8. Félicitations !

bcd986f7106d892b.gif

Félicitations, vous avez créé 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

Autre défi : vous pouvez utiliser Flutter FutureBuilder pour ajouter le type d'appareil actuel à l'UI de manière asynchrone. Si vous avez besoin d'aide, elle est implémentée dans le dossier contenant l'état terminé de l'atelier de programmation.

Documents de référence et étapes suivantes