Codelab multidispositivo Firebase

1. Introduzione

Ultimo aggiornamento: 2022-03-14

FlutterFire per la comunicazione tra dispositivi

Mentre assistiamo alla messa online di un gran numero di dispositivi domotici, indossabili e tecnologici per la salute personale, la comunicazione tra dispositivi diventa una parte sempre più importante nella creazione di applicazioni mobili. Configurare la comunicazione tra dispositivi, ad esempio controllare un browser da un'app per telefono o controllare ciò che viene riprodotto sulla TV dal telefono, è tradizionalmente più complesso rispetto alla creazione di una normale app mobile.

Il database Realtime di Firebase fornisce l' API Presence che consente agli utenti di vedere lo stato online/offline del proprio dispositivo; lo utilizzerai con il servizio Firebase Installations per monitorare e connettere tutti i dispositivi a cui ha effettuato l'accesso lo stesso utente. Utilizzerai Flutter per creare rapidamente applicazioni per più piattaforme e quindi creerai un prototipo cross-device che riproduce musica su un dispositivo e controlla la musica su un altro!

Cosa costruirai

In questo codelab costruirai un semplice telecomando per lettore musicale. La tua app:

  • Ottieni un semplice lettore musicale su Android, iOS e Web, realizzato con Flutter.
  • Consenti agli utenti di accedere.
  • Connetti i dispositivi quando lo stesso utente ha effettuato l'accesso su più dispositivi.
  • Consenti agli utenti di controllare la riproduzione della musica su un dispositivo da un altro dispositivo.

7f0279938e1d3ab5.gif

Cosa imparerai

  • Come creare ed eseguire un'app per 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.

Di cosa avrai bisogno

  • Un ambiente di sviluppo Flutter. Seguire le istruzioni nella guida all'installazione di Flutter per configurarlo.
  • È richiesta una versione minima di Flutter 2.10 o successiva. Se disponi di una versione precedente, esegui flutter upgrade.
  • Un account Firebase.

2. Preparazione

Ottieni il codice iniziale

Abbiamo creato un'app per lettore musicale in Flutter. Il codice iniziale si trova in un repository Git. Per iniziare, sulla riga di comando, clona il repository, 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

Costruisci l'app

Puoi lavorare con 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 essere in grado di vedere il seguente messaggio.

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

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

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

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

Configura un emulatore Android o un simulatore iOS

Se disponi già di un dispositivo Android o iOS da sviluppare, puoi saltare questo passaggio.

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

Per creare un simulatore iOS, avrai bisogno di un ambiente Mac. Scarica XCode e segui le istruzioni in Panoramica del simulatore > Usa il simulatore > Apri e chiudi un simulatore .

3. Configurazione di Firebase

Crea un progetto Firebase

Apri un browser su http://console.firebase.google.com/ .

  1. Accedi a Firebase .
  2. Nella console Firebase, fai clic su Aggiungi progetto (o Crea un progetto ) e assegna al progetto Firebase il nome Firebase-Cross-Device-Codelab .
  3. Fai clic sulle opzioni di creazione del progetto. Se richiesto, accetta i termini di Firebase. 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. Li configurerai quando inizializzerai FlutterFire.

Installa l'SDK di Firebase

Tornando alla riga di comando, nella directory del progetto, esegui il comando seguente 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 la CLI Firebase, puoi installarla eseguendo curl -sL https://firebase.tools | bash .
  2. Accedi eseguendo firebase login e seguendo le istruzioni.
  3. Installa la CLI FlutterFire eseguendo dart pub global activate flutterfire_cli .
  4. Configura la CLI FlutterFire eseguendo flutterfire configure .
  5. Al prompt, scegli il progetto che hai appena creato per questo codelab, qualcosa come Firebase-Cross-Device-Codelab .
  6. Seleziona iOS , Android e Web quando ti viene richiesto di scegliere il supporto della configurazione.
  7. Quando ti viene richiesto l' ID del bundle Apple , digita un dominio univoco oppure 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.

Nel tuo 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. Ma ora hai un'app Firebase e puoi iniziare a utilizzare i prodotti Firebase, tra cui:

  • Autenticazione Firebase , che consente ai tuoi utenti di accedere alla tua app.
  • Database in tempo reale Firebase (RTDB) ; utilizzerai l'API di presenza per monitorare lo stato online/offline del dispositivo
  • Le regole di sicurezza Firebase ti consentiranno di proteggere il database.
  • Servizio installazioni Firebase per identificare i dispositivi a cui ha effettuato l'accesso un singolo utente.

4. Aggiungi l'autenticazione Firebase

Abilita l'accesso e-mail per l'autenticazione Firebase

Per consentire agli utenti di accedere all'app Web, utilizzerai il metodo di accesso tramite email/password :

  1. Nella console Firebase, espandi il menu Crea nel pannello di sinistra.
  2. Fare clic su Autenticazione , quindi sul pulsante Inizia , quindi sulla scheda Metodo di accesso .
  3. Fare clic su E-mail/Password nell'elenco Provider di accesso , impostare l'interruttore Abilita sulla posizione attiva, quindi fare clic su Salva . 58e3e3e23c2f16a4.png

Configura l'autenticazione Firebase in Flutter

Sulla 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 possibile creare il flusso di accesso e disconnessione. 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, come l'accesso e la disconnessione. Scopri di più al riguardo nella documentazione sulla gestione dello stato di Flutter .

Incolla quanto segue 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 ApplicationState venga inizializzato all'avvio dell'app, aggiungerai 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'interfaccia utente dell'applicazione avrebbe dovuto rimanere la stessa, ma ora puoi consentire agli utenti di accedere e salvare gli stati dell'app.

Creare un flusso di accesso

In questo passaggio lavorerai sul flusso di accesso e disconnessione. Ecco come apparirà il flusso:

  1. Un utente disconnesso avvierà il flusso di accesso facendo clic sul menu contestuale 71fcc1030a336423.png sul lato destro della barra delle applicazioni.
  2. Il flusso di accesso verrà visualizzato in una finestra di dialogo.
  3. Se l'utente non ha mai effettuato l'accesso prima, gli verrà richiesto di creare un account utilizzando un indirizzo email valido e una password.
  4. Se l'utente ha già effettuato l'accesso, gli verrà richiesto di inserire la password.
  5. Una volta effettuato 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 al loginState dell'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 , creare 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,
          ),
        ),
      ]),
    );
  }
}

In terzo luogo, 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 essere in grado di vedere il menu contestuale 71fcc1030a336423.png sul lato destro della barra delle applicazioni. Facendo clic su di esso verrai indirizzato a una finestra di dialogo di accesso.

Una volta effettuato l'accesso con un indirizzo email valido e una password, dovresti essere in grado di vedere l'opzione Esci nel menu contestuale.

Nella console Firebase, sotto Authentication , dovresti essere in grado di vedere l'indirizzo email elencato come nuovo utente.

888506c86a28a72c.png

Congratulazioni! Gli utenti possono ora accedere all'app!

5. Aggiungi la connessione al database

Ora sei pronto per passare alla registrazione del dispositivo utilizzando l'API Firebase Presence.

Sulla riga di comando, esegui i seguenti comandi per aggiungere le dipendenze necessarie:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Creare una banca dati

Nella console Firebase,

  1. Passa alla sezione Realtime Database della console Firebase . Fare clic su Crea database .
  2. Se ti viene richiesto di selezionare una modalità di avvio per le tue regole di sicurezza, seleziona Modalità test per ora**.** (La modalità test crea regole di sicurezza che consentono il passaggio di tutte le richieste. Le regole di sicurezza verranno aggiunte in seguito. È importante non andare mai in produzione con le tue regole di sicurezza ancora in modalità test.)

Il database per ora è vuoto. Individua l'URL del tuo databaseURL in Impostazioni progetto , nella scheda Generale . Scorri verso il basso fino alla sezione App Web .

1b6076f60a36263b.png

Aggiungi l'URL del tuo databaseURL al file firebase_options.dart :

lib/firebase_options.dart

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

Registra i dispositivi utilizzando l'API RTDB Presence

Desideri registrare i dispositivi di un utente quando appaiono online. Per fare ciò, trarrai vantaggio dalle installazioni Firebase e dall'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, crea 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 sotto un ID utente nel tuo database.

5bef49cea3564248.png

6. Sincronizza 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 detterà gli stati sui dispositivi follower.

Crea un metodo setLeadDevice in application_state.dart e traccia 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 dell'app, crea un PopupMenuItem chiamato Controller modificando il widget SignedInMenuButton . Questo menu consentirà 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();
    }
  }
}

Scrive lo stato del dispositivo principale nel database

Dopo aver impostato un dispositivo principale, puoi sincronizzare gli stati del dispositivo principale con RTDB con il seguente codice. Aggiungere il seguente codice alla fine di application_state.dart. Questo inizierà a memorizzare due attributi: lo stato del giocatore (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

Sono presenti due parti per leggere e utilizzare lo stato del dispositivo principale. Innanzitutto, vuoi impostare un ascoltatore di database dello stato del giocatore principale in application_state . Questo ascoltatore dirà ai dispositivi follower quando aggiornare lo schermo tramite una richiamata. Nota che in questo passaggio hai definito un'interfaccia OnLeadDeviceChangeCallback . Non è ancora implementato; 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, avviare il listener del database durante l'inizializzazione del lettore in player_widget.dart . Passa la funzione _updatePlayer in modo che lo stato del player follower possa essere aggiornato ogni volta che cambia il valore del database.

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 sei pronto per testare l'app:

  1. Sulla 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 un emulatore Android. Vai al menu contestuale, scegli un'app come dispositivo leader. Dovresti essere in grado di vedere i lettori 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 i dispositivi follower aggiornarsi di conseguenza.

Se i dispositivi follower si aggiornano correttamente, sei riuscito a creare un controller multidispositivo. Resta solo un passo cruciale.

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! Pertanto, prima di finire, 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 l'utente che ha effettuato l'accesso a quel dispositivo. Nella console Firebase, vai al Realtime Database, quindi alla scheda Regole . Incolla le seguenti regole che consentono solo all'utente che ha effettuato l'accesso di leggere e scrivere gli stati del proprio dispositivo:

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

8. Congratulazioni!

bcd986f7106d892b.gif

Congratulazioni, hai creato con successo un telecomando multidispositivo utilizzando Flutter!

Crediti

Meglio insieme, una canzone di Firebase

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

9. Bonus

Come ulteriore sfida, prendi in considerazione l'utilizzo di Flutter FutureBuilder per aggiungere l'attuale tipo di dispositivo principale all'interfaccia utente in modo asincrono. Se hai bisogno di assistenza, è implementata nella cartella contenente lo stato finito del codelab.

Documenti di riferimento e passaggi successivi