Codelab cross-device Firebase

1. Introduzione

Ultimo aggiornamento: 14/03/2022

FlutterFire per la comunicazione cross-device

Con l'aumento del numero di dispositivi di domotica, indossabili e per la salute personale che si connettono a internet, la comunicazione cross-device diventa una parte sempre più importante della creazione di applicazioni mobile. La configurazione della comunicazione cross-device, ad esempio il controllo di un browser da un'app per smartphone o il controllo della riproduzione sulla TV dallo smartphone, è tradizionalmente più complessa rispetto alla creazione di una normale app mobile .

Realtime Database di Firebase fornisce l'API Presence , che consente agli utenti di visualizzare lo stato online/offline del proprio dispositivo. La utilizzerai con il servizio Firebase Installations per monitorare e connettere tutti i dispositivi su cui è stato eseguito l'accesso dallo stesso utente. Utilizzerai Flutter per creare rapidamente applicazioni per più piattaforme, quindi creerai un prototipo cross-device che riproduce musica su un dispositivo e la controlla su un altro.

Cosa creerai

In questo codelab, creerai un semplice telecomando per il lettore musicale. La tua app sarà in grado di:

  • Avere un semplice lettore musicale su Android, iOS e web, creato con Flutter.
  • Consenti agli utenti di accedere.
  • Collega i dispositivi quando lo stesso utente ha eseguito l'accesso su più dispositivi.
  • Consente agli utenti di controllare la riproduzione musicale su un dispositivo da un altro dispositivo.

7f0279938e1d3ab5.gif

Cosa imparerai a fare

  • Come creare ed eseguire un'app lettore musicale Flutter.
  • Come consentire agli utenti di accedere con Firebase Auth.
  • Come utilizzare l'API Firebase RTDB Presence e il servizio di installazione Firebase per connettere i dispositivi.

Che cosa ti serve

  • Un ambiente di sviluppo Flutter. Segui le istruzioni nella guida all'installazione di Flutter per configurarlo.
  • È richiesta una versione di Flutter pari ad almeno 2.10 o successive. Se hai una versione precedente, esegui flutter upgrade.
  • Un account Firebase.

2. Configurazione

Recupera il codice di avvio

Abbiamo creato un'app di riproduzione musicale in Flutter. Il codice iniziale si trova in un repository Git. Per iniziare, nella riga di comando clona il repository, sposta la cartella con lo stato iniziale e installa le dipendenze:

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

cd cross-device-controller/starter_code

flutter pub get

Crea l'app

Puoi utilizzare il tuo IDE preferito per creare l'app o utilizzare la riga di comando.

Nella directory dell'app, crea l'app per il web con il comando flutter run -d web-server.. Dovresti visualizzare il seguente prompt.

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

Visita http://localhost:<port> per visualizzare il lettore musicale.

Se hai familiarità con l'emulatore Android o il simulatore iOS, puoi creare l'app per queste piattaforme e installarla con il comando flutter run -d <device_name>.

L'app web dovrebbe mostrare un lettore musicale di base autonomo. Assicurati che le funzionalità del player funzionino come previsto. Si tratta di una semplice app di riproduzione musicale progettata per questo codelab. Può riprodurre solo un brano di Firebase, Better Together.

Configurare un emulatore Android o un simulatore iOS

Se hai già un dispositivo Android o iOS per lo sviluppo, puoi saltare questo passaggio.

Per creare un emulatore Android, scarica Android Studio, che supporta anche lo sviluppo di Flutter, e segui le istruzioni riportate in Creare e gestire dispositivi virtuali.

Per creare un simulatore iOS, è necessario un ambiente Mac. Scarica Xcode e segui le istruzioni riportate in Panoramica del simulatore > Utilizzare il simulatore > Aprire e chiudere un simulatore.

3. Configura Firebase

Crea un progetto Firebase

  1. Accedi alla console Firebase utilizzando il tuo Account Google.
  2. Fai clic sul pulsante per creare un nuovo progetto, quindi inserisci un nome per il progetto (ad esempio Firebase-Cross-Device-Codelab).
  3. Fai clic su Continua.
  4. Se richiesto, leggi e accetta i termini di Firebase, quindi fai clic su Continua.
  5. (Facoltativo) Attiva l'assistenza AI nella console Firebase (denominata "Gemini in Firebase").
  6. Per questo codelab non hai bisogno di Google Analytics, quindi disattiva l'opzione Google Analytics.
  7. Fai clic su Crea progetto, attendi il provisioning del progetto, poi fai clic su Continua.

Installa l'SDK Firebase

Di nuovo nella riga di comando, nella directory del progetto, esegui questo comando per installare Firebase:

flutter pub add firebase_core

Nel file pubspec.yaml, modifica la versione di firebase_core in modo che sia almeno 1.13.1 o esegui flutter upgrade

Inizializza FlutterFire

  1. Se non hai installato l'interfaccia a riga di comando di Firebase, puoi farlo eseguendo curl -sL https://firebase.tools | bash.
  2. Accedi eseguendo firebase login e seguendo le istruzioni.
  3. Installa l'interfaccia a riga di comando FlutterFire eseguendo dart pub global activate flutterfire_cli.
  4. Configura l'interfaccia a riga di comando FlutterFire eseguendo flutterfire configure.
  5. Al prompt, scegli il progetto che hai appena creato per questo codelab, ad esempio Firebase-Cross-Device-Codelab.
  6. Seleziona iOS, Android e Web quando ti viene chiesto di scegliere il supporto per la configurazione.
  7. Quando ti viene chiesto l'ID bundle Apple, digita un dominio univoco o inserisci com.example.appname, che va bene per lo scopo di questo codelab.

Una volta configurato, verrà generato un file firebase_options.dart contenente tutte le opzioni richieste per l'inizializzazione.

Nell'editor, aggiungi il seguente codice al file main.dart per inizializzare Flutter e 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 l'app con il comando:

flutter run

Non hai ancora modificato alcun elemento dell'interfaccia utente, quindi l'aspetto e il comportamento dell'app non sono cambiati. Ora hai un'app Firebase e puoi iniziare a utilizzare i prodotti Firebase, tra cui:

  • Firebase Authentication, che consente agli utenti di accedere alla tua app.
  • Firebase Realtime Database(RTDB): utilizzerai l'API Presence per monitorare lo stato online/offline del dispositivo
  • Le regole di sicurezza Firebase ti consentono di proteggere il database.
  • Servizio Firebase Installations per identificare i dispositivi a cui ha eseguito l'accesso un singolo utente.

4. Aggiungi Firebase Auth

Abilitare l'accesso con indirizzo email per Firebase Authentication

Per consentire agli utenti di accedere all'app web, utilizzerai il metodo di accesso Email/password:

  1. Nella console Firebase, espandi il menu Build nel riquadro a sinistra.
  2. Fai clic su Autenticazione, quindi sul pulsante Inizia e sulla scheda Metodo di accesso.
  3. Fai clic su Email/Password nell'elenco Provider di accesso, imposta l'opzione Attiva su ON e poi fai clic su Salva. 58e3e3e23c2f16a4.png

Configurare Firebase Authentication in Flutter

Nella riga di comando, esegui i seguenti comandi per installare i pacchetti Flutter necessari:

flutter pub add firebase_auth

flutter pub add provider

Con questa configurazione, ora puoi creare il flusso di accesso e disconnessione. Poiché lo stato di autenticazione non deve cambiare da una schermata all'altra, creerai una classe application_state.dart per tenere traccia delle modifiche dello stato a livello di app, ad esempio l'accesso e la disconnessione. Scopri di più in merito nella documentazione relativa alla gestione dello stato di Flutter.

Incolla il seguente codice nel nuovo file 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();
  }
}

Per assicurarti che ApplicationState venga inizializzato all'avvio dell'app, aggiungi un passaggio di inizializzazione 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(),
  ));
}

Anche in questo caso, la UI dell'applicazione dovrebbe essere rimasta invariata, ma ora puoi consentire agli utenti di accedere e salvare gli stati dell'app.

Crea un flusso di accesso

In questo passaggio, lavorerai al flusso di accesso e disconnessione. Ecco come si presenta il flusso:

  1. Un utente che ha eseguito la disconnessione avvia il flusso di accesso facendo clic sul menu contestuale 71fcc1030a336423.png sul lato destro della barra delle app.
  2. Il flusso di accesso verrà visualizzato in una finestra di dialogo.
  3. Se l'utente non ha mai eseguito l'accesso, gli verrà chiesto di creare un account utilizzando un indirizzo email valido e una password.
  4. Se l'utente ha già eseguito l'accesso, gli verrà chiesto di inserire la password.
  5. Una volta effettuato l'accesso, se l'utente fa clic sul menu contestuale, viene visualizzata l'opzione Esci.

c295f6fa2e1d40f3.png

L'aggiunta del flusso di accesso richiede tre passaggi.

Innanzitutto, crea un widget AppBarMenuButton. Questo widget controllerà il popup del menu contestuale in base al loginState di un utente. Aggiungi le importazioni

lib/src/widgets.dart

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

Aggiungi il seguente codice 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),
        ),
      ),
    ];
  }
}

Secondo, nella stessa classe widgets.dart, crea il 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,
          ),
        ),
      ]),
    );
  }
}

Terzo, trova il widget della barra delle app esistente in main.dart. Aggiungi AppBarMenuButton per visualizzare l'opzione Accedi o Esci.

lib/main.dart

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

Esegui il comando flutter run per riavviare l'app con queste modifiche. Dovresti visualizzare il menu contestuale 71fcc1030a336423.png sul lato destro della barra delle app. Se fai clic, si aprirà una finestra di dialogo di accesso.

Una volta eseguito l'accesso con un indirizzo email e una password validi, dovresti visualizzare l'opzione Esci nel menu contestuale.

Nella Console Firebase, in Autenticazione, dovresti visualizzare l'indirizzo email elencato come nuovo utente.

888506c86a28a72c.png

Complimenti! Ora gli utenti possono accedere all'app.

5. Aggiungi connessione al database

Ora puoi passare alla registrazione del dispositivo utilizzando l'API Firebase Presence.

Nella riga di comando, esegui questi comandi per aggiungere le dipendenze necessarie:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Crea un database

Nella console Firebase,

  1. Vai alla sezione Realtime Database della console Firebase. Fai clic su Crea database.
  2. Se ti viene chiesto di selezionare una modalità di avvio per le regole di sicurezza, seleziona per ora Modalità test**.** La modalità test crea regole di sicurezza che consentono il passaggio di tutte le richieste. Aggiungerai le regole di sicurezza in un secondo momento. È importante non passare mai alla produzione con le regole di sicurezza ancora in modalità test.)

Per il momento il database è vuoto. Individua il tuo databaseURL in Impostazioni progetto, nella scheda Generale. Scorri verso il basso fino alla sezione App web.

1b6076f60a36263b.png

Aggiungere databaseURL al file firebase_options.dart:

lib/firebase_options.dart

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

Registrare i dispositivi utilizzando l'API RTDB Presence

Vuoi registrare i dispositivi di un utente quando vengono visualizzati online. Per farlo, sfrutterai Firebase Installations e l'API Firebase RTDB Presence per tenere traccia di un elenco di dispositivi online di un singolo utente. Il seguente codice ti aiuterà a raggiungere questo obiettivo:

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

Torna alla riga di comando, compila ed esegui l'app sul tuo dispositivo o in un browser con flutter run.

Nella tua app, accedi come utente. Ricordati di accedere come lo stesso utente su piattaforme diverse.

Nella console Firebase, dovresti vedere i tuoi dispositivi visualizzati con un ID utente nel database.

5bef49cea3564248.png

6. Sincronizzare lo stato del dispositivo

Seleziona un dispositivo principale

Per sincronizzare gli stati tra i dispositivi, designa un dispositivo come leader o controller. Il dispositivo principale determinerà gli stati sui dispositivi secondari.

Crea un metodo setLeadDevice in application_state.dart e monitora questo dispositivo con la chiave active_device in 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;
      });
    }
  }

Per aggiungere questa funzionalità al menu contestuale della barra delle app, crea un PopupMenuItem chiamato Controller modificando il widget SignedInMenuButton. Questo menu consente agli utenti di impostare il dispositivo principale.

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

Scrivere lo stato del dispositivo principale nel database

Dopo aver impostato un dispositivo principale, puoi sincronizzare i relativi stati con RTDB utilizzando il seguente codice. Aggiungi il seguente codice alla fine di application_state.dart.In questo modo, inizierai a memorizzare due attributi: lo stato del lettore (riproduzione o pausa) e la posizione del cursore.

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

Infine, devi chiamare setActiveDeviceState ogni volta che lo stato del giocatore del controller viene aggiornato. Apporta le seguenti modifiche al file player_widget.dart esistente:

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

Leggere lo stato del dispositivo principale dal database

Per leggere e utilizzare lo stato del dispositivo principale, sono necessarie due parti. Innanzitutto, devi configurare un listener del database dello stato del giocatore principale in application_state. Questo listener comunicherà ai dispositivi follower quando aggiornare lo schermo tramite un callback. Tieni presente che in questo passaggio hai definito un'interfaccia OnLeadDeviceChangeCallback. Non è ancora implementata. Implementerai questa interfaccia in player_widget.dart nel passaggio successivo.

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

In secondo luogo, avvia l'ascoltatore del database durante l'inizializzazione del giocatore in player_widget.dart. Passa la funzione _updatePlayer in modo che lo stato del giocatore follower possa essere aggiornato ogni volta che il valore del database cambia.

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

Ora puoi testare l'app:

  1. Nella riga di comando, esegui l'app su emulatori e/o in un browser con: flutter run -d <device-name>
  2. Apri le app in un browser, in un simulatore iOS o in un emulatore Android. Vai al menu contestuale e scegli un'app da impostare come dispositivo principale. Dovresti vedere i lettori dei dispositivi follower cambiare man mano che il dispositivo leader si aggiorna.
  3. Ora cambia il dispositivo principale, riproduci o metti in pausa la musica e osserva l'aggiornamento dei dispositivi secondari.

Se i dispositivi follower si aggiornano correttamente, hai creato un controller cross-device. Manca solo un passaggio fondamentale.

7. Aggiorna le regole di sicurezza

A meno che non scriviamo regole di sicurezza migliori, qualcuno potrebbe scrivere uno stato su un dispositivo che non possiede. Prima di terminare, aggiorna le regole di sicurezza del database in tempo reale per assicurarti che gli unici utenti che possono leggere o scrivere su un dispositivo siano quelli che hanno eseguito l'accesso al dispositivo. Nella console Firebase, vai a Realtime Database, quindi alla scheda Regole. Incolla le seguenti regole che consentono solo all'utente che ha eseguito l'accesso di leggere e scrivere gli stati dei propri dispositivi:

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

8. Complimenti!

bcd986f7106d892b.gif

Congratulazioni, hai creato correttamente un telecomando cross-device utilizzando Flutter.

Crediti

Better Together, una canzone di Firebase

  • Musiche di Ryan Vernon
  • Testi e copertina dell'album di Marissa Christy
  • Voce di JP Gomez

9. Bonus

Come ulteriore sfida, valuta la possibilità di utilizzare Flutter FutureBuilder per aggiungere in modo asincrono il tipo di dispositivo principale corrente alla UI. Se hai bisogno di assistenza, è implementata nella cartella contenente lo stato finale del codelab.

Documenti di riferimento e passaggi successivi