Codelab cross-device Firebase

1. Introduzione

Ultimo aggiornamento: 14/03/2022

FlutterFire per la comunicazione cross-device

Mentre assistiamo a un gran numero di dispositivi di domotica, dispositivi indossabili e tecnologici per la salute personale che entrano online, la comunicazione cross-device diventa una parte sempre più importante della creazione di applicazioni mobile. La configurazione della comunicazione tra dispositivi, 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.

Firebase Realtime Database fornisce l'API Presence che consente agli utenti di vedere lo stato online/offline del proprio dispositivo. La utilizzerai con il servizio di installazione di Firebase per monitorare e collegare tutti i dispositivi su cui lo stesso utente ha eseguito l'accesso. Utilizzerai Flutter per creare rapidamente applicazioni per più piattaforme e poi realizzerai un prototipo cross-device che riproduce la 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 media player su Android, iOS e web, creato con Flutter.
  • Consenti agli utenti di accedere.
  • Connetti i dispositivi su più dispositivi quando lo stesso utente ha eseguito l'accesso.
  • Consenti agli utenti di controllare la riproduzione di musica su un dispositivo da un altro dispositivo.

7f0279938e1d3ab5.gif

Cosa imparerai a fare

  • Come creare ed eseguire un'app per il music player 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 minima di Flutter 2.10 o successiva. Se hai una versione precedente, esegui flutter upgrade.
  • Un account Firebase.

2. Preparazione

Ottieni il codice di avvio

Abbiamo creato un'app per la musica in Flutter. Il codice di avvio si trova in un repository Git. Per iniziare, clona il repository nella riga di comando, spostati nella 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 la riga di comando.

Nella directory delle 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 player musicale.

Se hai dimestichezza 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&#39;app web dovrebbe mostrare un semplice player musicale autonomo. Assicurati che le funzionalità del player funzionino come previsto. Si tratta di una semplice app di lettore musicale progettata per questo codelab. Può riprodurre solo un brano 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 Flutter, e segui le istruzioni riportate in Creare e gestire dispositivi virtuali.

Per creare un simulatore iOS, devi avere un ambiente Mac. Scarica XCode e segui le istruzioni riportate in Panoramica del simulatore > Utilizza il simulatore > Apri e chiudi un simulatore.

3. Configurazione di Firebase

Crea un progetto Firebase

Apri un browser e vai all'indirizzo http://console.firebase.google.com/.

  1. Accedi a Firebase.
  2. Nella console Firebase, fai clic su Aggiungi progetto (o Crea un progetto) e denomina il tuo progetto Firebase Firebase-Cross-Device-Codelab.
  3. Fai clic sulle opzioni di creazione del progetto. Accetta i termini di Firebase, se richiesto. Salta la configurazione di Google Analytics perché non utilizzerai Analytics per questa app.

Non è necessario scaricare i file menzionati o modificare i file build.gradle. Le configurerai al momento dell'inizializzazione di FlutterFire.

Installa l'SDK Firebase

Torna alla directory del progetto della riga di comando ed 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 oppure 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 viene richiesto l'ID pacchetto Apple, digita un dominio univoco o inserisci com.example.appname, che va bene ai fini di questo codelab.

Una volta configurato, verrà generato un file firebase_options.dart contenente tutte le opzioni necessarie 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 elementi dell&#39;interfaccia utente, pertanto l&#39;aspetto e il comportamento dell&#39;app non sono cambiati. Ora però hai un&#39;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 di Firebase ti consentono di proteggere il database.
  • Servizio installazioni Firebase per identificare i dispositivi a cui ha eseguito l'accesso un singolo utente.

4. Aggiungi autenticazione Firebase

Attivare l'accesso via 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 Crea nel riquadro a sinistra.
  2. Fai clic su Autenticazione, quindi sul pulsante Inizia e infine sulla scheda Metodo di accesso.
  3. Fai clic su Email/Password nell'elenco Provider di accesso, imposta l'opzione Abilita su On e fai clic su Salva. 58e3e3e23c2f16a4.png

Configurare Firebase Authentication in Flutter

Nella riga di comando, esegui questi 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 uscita. Poiché lo stato di autenticazione non dovrebbe 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 accesso e uscita. Per saperne di più, consulta la documentazione sulla gestione dello stato fluido.

Incolla il codice seguente 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, dovrai aggiungere 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, l&#39;interfaccia utente dell&#39;applicazione dovrebbe essere rimasta invariata, ma ora puoi consentire agli utenti di accedere e salvare gli stati dell&#39;app.

Crea un flusso di accesso

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

  1. Un utente che non ha eseguito l'accesso avvia il flusso di accesso facendo clic sul menu contestuale 71fcc1030a336423.pngsul 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 prima d'ora, gli verrà chiesto di creare un account utilizzando un indirizzo email e una password validi.
  4. Se l'utente ha già eseguito l'accesso, gli verrà chiesto di inserire la password.
  5. Dopo che l'utente avrà eseguito l'accesso, facendo clic sul menu contestuale verrà visualizzata l'opzione Esci.

c295f6fa2e1d40f3.png

L'aggiunta del flusso di accesso richiede tre passaggi.

Prima di tutto, crea un widget AppBarMenuButton. Questo widget controllerà il popup del menu contestuale in base alle 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),
        ),
      ),
    ];
  }
}

In secondo luogo, 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 appBar 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 riuscire a vedere il menu contestuale 71fcc1030a336423.png sul lato destro della barra delle app. Se fai clic, si aprirà una finestra di dialogo di accesso.

Dopo aver eseguito l'accesso con un indirizzo email e una password validi, dovresti visualizzare l'opzione Uscire nel menu contestuale.

Nella Console Firebase, in Autenticazione, dovresti riuscire a vedere 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&#39;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 di Firebase. Fai clic su Crea database.
  2. Se ti viene chiesto di selezionare una modalità iniziale per le regole di sicurezza, seleziona per ora la modalità di test**.** La modalità di 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 in produzione con le regole di sicurezza ancora in modalità di test.)

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

1b6076f60a36263b.png

Aggiungi 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 sono online. A questo scopo, utilizzerai le installazioni di Firebase e l&#39;API Firebase RTDB Presence per tenere traccia di un elenco di dispositivi online di un singolo utente. Il seguente codice 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 e avvia l'app sul tuo dispositivo o in un browser con flutter run.

Accedi all'app come utente. Ricordati di accedere come lo stesso utente su piattaforme diverse.

Nella Console Firebase, dovresti vedere i tuoi dispositivi in un ID utente nel database.

5bef49cea3564248.png

6. Sincronizza lo stato del dispositivo

Seleziona un dispositivo principale

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

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 denominato 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

Una volta impostato un dispositivo lead, puoi sincronizzarne gli 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 player (riproduzione o messa in 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 player del controller si aggiorna. 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

È necessario leggere e utilizzare lo stato del dispositivo lead in due parti. Innanzitutto, devi configurare un listener del database dello stato del lead player in application_state. Questo listener indica ai dispositivi follower quando aggiornare lo schermo tramite un callback. Tieni presente che in questo passaggio hai definito un'interfaccia OnLeadDeviceChangeCallback. Non è ancora implementata; la implementerai 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();
      });
    }
  }

Poi, avvia il listener del database durante l'inizializzazione del player 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 è tutto pronto per testare l'app:

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

Se i dispositivi dei follower si aggiornano correttamente, sei riuscito a creare un controller cross-device. Manca solo un passaggio fondamentale.

7. Aggiorna regole di sicurezza

Se non creiamo regole di sicurezza migliori, qualcuno potrebbe scrivere uno stato su un dispositivo di cui non è proprietario. 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 al Realtime Database, quindi alla scheda Regole. Incolla le seguenti regole che consentono solo all'utente che ha eseguito l'accesso di leggere e scrivere i propri stati del dispositivo:

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

8. Complimenti!

bcd986f7106d892b.gif

Complimenti, hai creato un telecomando cross-device utilizzando Flutter.

Crediti

Better Together, un brano Firebase

  • Musica di Ryan Vernon
  • Testo e copertina dell'album di Marissa Christy
  • Voce di JP Gomez

9. Bonus

Come ulteriore verifica, valuta la possibilità di utilizzare Flutter FutureBuilder per aggiungere il tipo di dispositivo lead attuale alla UI in modo asincrono. Se hai bisogno di assistenza, questa viene implementata nella cartella contenente lo stato Completato del codelab.

Documenti di riferimento e passaggi successivi