Geräteübergreifendes Firebase-Codelab

1. Einführung

Zuletzt aktualisiert: 14.03.2022

FlutterFire für die geräteübergreifende Kommunikation

Da immer mehr Heimautomatisierungs-, Wearables- und persönliche Gesundheitstechnologie-Geräte online gehen, wird die geräteübergreifende Kommunikation zu einem immer wichtigeren Bestandteil der Entwicklung mobiler Apps. Die Einrichtung der geräteübergreifenden Kommunikation, z. B. die Steuerung eines Browsers über eine Telefon-App oder die Steuerung der Wiedergabe auf Ihrem Fernseher über Ihr Telefon, ist üblicherweise komplexer als das Erstellen einer normalen mobilen App .

Die Realtime Database von Firebase bietet die Presence API , mit der Nutzer den Online-/Offlinestatus ihres Geräts sehen können. Sie verwenden die Datenbank zusammen mit dem Firebase-Installationsdienst, um alle Geräte, auf denen sich ein Nutzer angemeldet hat, zu erfassen und zu verbinden. Sie verwenden Flutter, um schnell Anwendungen für mehrere Plattformen zu erstellen. Anschließend erstellen Sie einen geräteübergreifenden Prototyp, der Musik auf einem Gerät abspielt und die Musik auf einem anderen Gerät steuert.

Umfang

In diesem Codelab erstellen Sie eine einfache Fernbedienung für einen Musikplayer. Mit der Anwendung können Sie Folgendes tun:

  • Sie haben einen einfachen Musikplayer für Android, iOS und das Web, der mit Flutter entwickelt wurde.
  • Nutzern die Anmeldung erlauben.
  • Geräte verbinden, wenn derselbe Nutzer auf mehreren Geräten angemeldet ist
  • Nutzern erlauben, die Musikwiedergabe auf einem Gerät von einem anderen Gerät aus zu steuern

7f0279938e1d3ab5.gif

Aufgaben in diesem Lab

  • So erstellen und führen Sie eine Flutter-Musikplayer-App aus.
  • Nutzern erlauben, sich mit Firebase Auth anzumelden
  • Informationen zum Verbinden von Geräten mithilfe der Firebase RTDB Presence API und des Firebase Installation Service

Voraussetzungen

  • Eine Flutter-Entwicklungsumgebung. Folgen Sie der Anleitung in der Installationsanleitung für Flutter, um das Flutter-Gerät einzurichten.
  • Sie benötigen mindestens Flutter-Version 2.10. Wenn Sie eine niedrigere Version haben, führen Sie flutter upgrade. aus.
  • Ein Firebase-Konto

2. Einrichtung

Startcode abrufen

Wir haben eine Musikplayer-App in Flutter entwickelt. Der Startcode befindet sich in einem Git-Repository. Zuerst müssen Sie das Repository in der Befehlszeile klonen, in den Ordner mit dem Startstatus wechseln und die Abhängigkeiten installieren:

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

cd cross-device-controller/starter_code

flutter pub get

App erstellen

Sie können die Anwendung mit Ihrer bevorzugten IDE erstellen oder die Befehlszeile verwenden.

Erstellen Sie die Web-App in Ihrem App-Verzeichnis mit dem Befehl flutter run -d web-server.Sie sollten die folgende Eingabeaufforderung sehen.

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

Gehe zu http://localhost:<port>, um den Musikplayer anzusehen.

Wenn Sie mit dem Android-Emulator oder iOS-Simulator vertraut sind, können Sie die App für diese Plattformen erstellen und mit dem Befehl flutter run -d <device_name> installieren.

Die Webanwendung sollte einen einfachen eigenständigen Musikplayer anzeigen. Prüfen, ob die Playerfunktionen wie vorgesehen funktionieren Dies ist eine einfache Musikplayer-App, die für dieses Codelab entwickelt wurde. Es kann nur der Firebase-Song Better Together abgespielt werden.

Android-Emulator oder iOS-Simulator einrichten

Wenn Sie bereits ein Android- oder iOS-Gerät für die Entwicklung haben, können Sie diesen Schritt überspringen.

Wenn Sie einen Android-Emulator erstellen möchten, laden Sie Android Studio herunter, das auch die Flutter-Entwicklung unterstützt, und folgen Sie der Anleitung unter Virtuelle Geräte erstellen und verwalten.

Zum Erstellen eines iOS-Simulators benötigen Sie eine Mac-Umgebung. Laden Sie XCode herunter und folgen Sie der Anleitung unter Simulatorübersicht > „Simulator verwenden“ > Simulator öffnen und schließen.

3. Firebase einrichten

Firebase-Projekt erstellen

Öffnen Sie http://console.firebase.google.com/ in einem Browser.

  1. Melden Sie sich in Firebase an.
  2. Klicken Sie in der Firebase Console auf Projekt hinzufügen (oder Projekt erstellen) und geben Sie als Namen für Ihr Firebase-Projekt Firebase-Cross-Device-Codelab ein.
  3. Klicken Sie sich durch die Optionen für die Projekterstellung. Akzeptieren Sie die Firebase-Nutzungsbedingungen, wenn Sie dazu aufgefordert werden. Überspringen Sie die Einrichtung von Google Analytics, weil Sie Analytics für diese App nicht verwenden werden.

Sie müssen die genannten Dateien nicht herunterladen oder die build.gradle-Dateien ändern. Diese konfigurieren Sie, wenn Sie FlutterFire initialisieren.

Firebase SDK installieren

Führen Sie in der Befehlszeile im Projektverzeichnis den folgenden Befehl aus, um Firebase zu installieren:

flutter pub add firebase_core

Bearbeiten Sie in der Datei pubspec.yaml die Version für firebase_core auf 1.13.1 oder führen Sie flutter upgrade aus

FlutterFire initialisieren

  1. Wenn Sie die Firebase CLI nicht installiert haben, können Sie sie installieren, indem Sie curl -sL https://firebase.tools | bash ausführen.
  2. Melde dich an, indem du firebase login ausführst und den Aufforderungen folgst.
  3. Installieren Sie die FlutterFire CLI, indem Sie dart pub global activate flutterfire_cli ausführen.
  4. Konfigurieren Sie die FlutterFire-Befehlszeile, indem Sie flutterfire configure ausführen.
  5. Wählen Sie in der Eingabeaufforderung das Projekt aus, das Sie gerade für dieses Codelab erstellt haben, z. B. Firebase-Cross-Device-Codelab.
  6. Wählen Sie iOS, Android und Web aus, wenn Sie aufgefordert werden, die Unterstützung für die Konfiguration festzulegen.
  7. Wenn Sie nach der Apple-Paket-ID gefragt werden, geben Sie eine eindeutige Domain oder com.example.appname ein. Dies reicht für dieses Codelab aus.

Nach der Konfiguration wird eine firebase_options.dart-Datei für Sie generiert, die alle für die Initialisierung erforderlichen Optionen enthält.

Fügen Sie der Datei „main.dart“ im Editor den folgenden Code hinzu, um Flutter und Firebase zu initialisieren:

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

Kompilieren Sie die App mit dem folgenden Befehl:

flutter run

Sie haben noch keine UI-Elemente geändert, sodass sich das Aussehen und Verhalten der App nicht verändert hat. Jetzt haben Sie aber eine Firebase-App und können Firebase-Produkte verwenden, darunter:

  • Firebase Authentication ermöglicht es Ihren Nutzern, sich in Ihrer App anzumelden.
  • Firebase Realtime Database(RTDB): Sie verwenden die Presence API, um den Online-/Offlinestatus des Geräts zu verfolgen.
  • Mit Firebase-Sicherheitsregeln können Sie die Datenbank schützen.
  • Firebase-Installationsdienst zur Identifizierung der Geräte, auf denen sich ein einzelner Nutzer angemeldet hat.

4. Firebase Auth hinzufügen

E-Mail-Anmeldung für Firebase Authentication aktivieren

Damit sich Nutzer in der Web-App anmelden können, verwenden Sie die Anmeldemethode E-Mail-Adresse/Passwort:

  1. Maximieren Sie in der Firebase Console im linken Bereich das Menü Build.
  2. Klicken Sie auf Authentication (Authentifizierung), dann auf die Schaltfläche Get Started (Jetzt starten) und dann auf den Tab Sign-in method (Anmeldemethode).
  3. Klicken Sie in der Liste Anbieter für Anmeldungen auf E-Mail-Adresse/Passwort, stellen Sie den Schalter für Aktivieren auf „Ein“ und klicken Sie auf Speichern. 58e3e3e23c2f16a4.png

Firebase-Authentifizierung in Flutter konfigurieren

Führen Sie in der Befehlszeile die folgenden Befehle aus, um die erforderlichen Flutter-Pakete zu installieren:

flutter pub add firebase_auth

flutter pub add provider

Mit dieser Konfiguration können Sie jetzt den An- und Abmeldevorgang erstellen. Da sich der Authentifizierungsstatus nicht von Bildschirm zu Bildschirm ändern sollte, erstellen Sie eine application_state.dart-Klasse, um Statusänderungen auf App-Ebene zu verfolgen, z. B. Anmeldung und Abmeldung. Weitere Informationen hierzu finden Sie in der Dokumentation zur Flutter-Statusverwaltung.

Fügen Sie Folgendes in die neue Datei application_state.dart ein:

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

Damit ApplicationState beim Start der App initialisiert wird, musst du main.dart einen Initialisierungsschritt hinzufügen:

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

Die Benutzeroberfläche der Anwendung sollte unverändert geblieben sein. Jetzt können Sie jedoch Nutzer anmelden und App-Status speichern.

Anmeldevorgang erstellen

In diesem Schritt arbeiten Sie am Anmelde- und Abmeldevorgang. So sieht der Ablauf aus:

  1. Ein abgemeldeter Nutzer startet den Anmeldevorgang, indem er rechts in der App-Leiste auf das Kontextmenü 71fcc1030a336423.pngklickt.
  2. Der Anmeldevorgang wird in einem Dialogfeld angezeigt.
  3. Wenn sich der Nutzer noch nie angemeldet hat, wird er aufgefordert, ein Konto mit einer gültigen E-Mail-Adresse und einem Passwort zu erstellen.
  4. Wenn sich der Nutzer schon einmal angemeldet hat, wird er aufgefordert, sein Passwort einzugeben.
  5. Sobald der Nutzer angemeldet ist, wird durch Klicken auf das Kontextmenü die Option Abmelden angezeigt.

c295f6fa2e1d40f3.png

Das Hinzufügen des Anmeldevorgangs umfasst drei Schritte.

Erstellen Sie zuerst ein AppBarMenuButton-Widget. Dieses Widget steuert das Pop-up des Kontextmenüs in Abhängigkeit von den loginState eines Nutzers. Importe hinzufügen

lib/src/widgets.dart

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

Hängen Sie den folgenden Code an widgets.dart. an

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),
        ),
      ),
    ];
  }
}

Erstellen Sie dann in derselben widgets.dart-Klasse das SignInDialog-Widget.

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,
          ),
        ),
      ]),
    );
  }
}

Suchen Sie im dritten Schritt das vorhandene appBar-Widget unter main.dart.. Fügen Sie AppBarMenuButton hinzu, damit die Option Anmelden oder Abmelden angezeigt wird.

lib/main.dart

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

Führen Sie den Befehl flutter run aus, um die App mit diesen Änderungen neu zu starten. Rechts in der App-Leiste sollte das Kontextmenü 71fcc1030a336423.png angezeigt werden. Wenn Sie darauf klicken, wird ein Anmeldedialogfeld geöffnet.

Sobald Sie sich mit einer gültigen E-Mail-Adresse und einem Passwort angemeldet haben, sollte im Kontextmenü die Option Abmelden angezeigt werden.

In der Firebase Console sollte unter Authentifizierung die E-Mail-Adresse als neuer Nutzer aufgeführt sein.

888506c86a28a72c.png

Glückwunsch! Nutzer können sich jetzt in der App anmelden.

5. Datenbankverbindung hinzufügen

Jetzt können Sie mit der Geräteregistrierung mithilfe der Firebase Presence API fortfahren.

Führen Sie in der Befehlszeile die folgenden Befehle aus, um die erforderlichen Abhängigkeiten hinzuzufügen:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Datenbank erstellen

In der Firebase Console

  1. Gehen Sie in der Firebase Console zum Abschnitt Echtzeitdatenbank. Klicken Sie auf Datenbank erstellen.
  2. Wenn Sie aufgefordert werden, einen Startmodus für Ihre Sicherheitsregeln auszuwählen, wählen Sie vorerst Testmodus aus. Im Testmodus werden Sicherheitsregeln erstellt, die alle Anfragen zulassen. Sicherheitsregeln werden später hinzugefügt. Es ist wichtig, dass Sie nie in die Produktion gehen, wenn sich Ihre Sicherheitsregeln noch im Testmodus befinden.)

Die Datenbank ist vorerst leer. Sie finden Ihre databaseURL in den Projekteinstellungen auf dem Tab Allgemein. Scrollen Sie nach unten zum Abschnitt Web-Apps.

1b6076f60a36263b.png

Fügen Sie die databaseURL zur Datei firebase_options.dart hinzu.:

lib/firebase_options.dart

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

Geräte mit der RTDB Presence API registrieren

Sie möchten die Geräte eines Nutzers registrieren, wenn sie online sind. Dazu verwenden Sie Firebase-Installationen und die Firebase RTDB-Präsenz-API, um eine Liste der Onlinegeräte eines einzelnen Nutzers im Blick zu behalten. Mit dem folgenden Code können Sie dieses Ziel erreichen:

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

Kehren Sie zur Befehlszeile zurück, erstellen Sie die App mit flutter run. und führen Sie sie auf Ihrem Gerät oder in einem Browser aus

Melden Sie sich in Ihrer App als Nutzer an. Melden Sie sich auf verschiedenen Plattformen als derselbe Nutzer an.

In der Firebase Console sollten Ihre Geräte in Ihrer Datenbank unter einer Nutzer-ID aufgeführt sein.

5bef49cea3564248.png

6. Gerätestatus synchronisieren

Leitgerät auswählen

Legen Sie ein Gerät als Leader oder Controller fest, um den Status zwischen Geräten zu synchronisieren. Das Hauptgerät bestimmt die Status der Folgegeräte.

Erstellen Sie eine setLeadDevice-Methode in application_state.dart und erfassen Sie dieses Gerät mit dem Schlüssel active_device in der 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;
      });
    }
  }

Wenn Sie diese Funktion dem Kontextmenü der App-Leiste hinzufügen möchten, erstellen Sie eine PopupMenuItem mit dem Namen Controller, indem Sie das SignedInMenuButton-Widget ändern. In diesem Menü können Nutzer das Hauptgerät festlegen.

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

Status des Hauptgeräts in die Datenbank schreiben

Nachdem du ein Hauptgerät festgelegt hast, kannst du die Status des Hauptgeräts mit dem folgenden Code mit RTDB synchronisieren. Hängen Sie den folgenden Code an das Ende von application_state.dart. an. Dadurch werden zwei Attribute gespeichert: der Player-Status (Wiedergabe oder Pause) und die Position des Schiebereglers.

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

Außerdem musst du setActiveDeviceState jedes Mal aufrufen, wenn sich der Spielerstatus des Controllers ändert. Nehmen Sie die folgenden Änderungen an der vorhandenen Datei player_widget.dart vor:

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

Status des Lead-Geräts aus der Datenbank auslesen

Der Status des Lead-Geräts wird in zwei Teilen gelesen und verwendet. Zuerst richten Sie einen Datenbank-Listener für den Status des leitenden Spielers in application_state ein. Dieser Listener informiert die folgenden Geräte über einen Callback, wann der Bildschirm aktualisiert werden soll. Beachten Sie, dass Sie in diesem Schritt eine Schnittstelle OnLeadDeviceChangeCallback definiert haben. Sie ist noch nicht implementiert. Sie implementieren diese Schnittstelle im nächsten Schritt in player_widget.dart.

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

Zweitens: Starte den Datenbank-Listener während der Initialisierung des Players in player_widget.dart. Übergebe die Funktion _updatePlayer, damit der Spielerstatus des Kanalmitglieds aktualisiert werden kann, wenn sich der Datenbankwert ändert.

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

Jetzt können Sie die App testen:

  1. Führen Sie die App in der Befehlszeile in Emulatoren und/oder in einem Browser mit folgendem Befehl aus: flutter run -d <device-name>
  2. Öffnen Sie die Apps in einem Browser, in einem iOS-Simulator oder in einem Android-Emulator. Öffnen Sie das Kontextmenü und wählen Sie eine App als Leitgerät aus. Du solltest sehen können, wie sich die Spieler der folgenden Geräte ändern, wenn das Gerät aktualisiert wird.
  3. Ändern Sie jetzt das Hauptgerät, spielen Sie Musik ab oder pausieren Sie sie und beobachten Sie, wie sich die Folgegeräte entsprechend aktualisieren.

Wenn die folgenden Geräte ordnungsgemäß aktualisiert werden, ist ein geräteübergreifender Controller erfolgreich erstellt. Jetzt ist nur noch ein wichtiger Schritt übrig.

7. Sicherheitsregeln aktualisieren

Wenn wir keine besseren Sicherheitsregeln schreiben, könnte jemand einen Status auf einem fremden Gerät schreiben. Bevor Sie fertig sind, aktualisieren Sie die Sicherheitsregeln für die Realtime Database, damit nur die Nutzer auf einem Gerät Lese- oder Schreibvorgänge durchführen können, die auf diesem Gerät angemeldet sind. Gehen Sie in der Firebase Console zur Echtzeitdatenbank und dann zum Tab Regeln. Fügen Sie die folgenden Regeln ein, damit nur angemeldete Nutzer ihre eigenen Gerätestatus lesen und schreiben können:

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

8. Glückwunsch!

bcd986f7106d892b.gif

Glückwunsch, Sie haben mit Flutter eine geräteübergreifende Fernbedienung erstellt.

Gutschriften

Better Together, ein Firebase-Song

  • Musik von Ryan Vernon
  • Lyrics und Albumcover von Marissa Christy
  • Voice von JP Gomez

9. Bonus

Als zusätzliche Herausforderung können Sie Flutter FutureBuilder verwenden, um den aktuellen Gerätetyp des Leads asynchron zur Benutzeroberfläche hinzuzufügen. Falls Sie eine Unterstützung benötigen, finden Sie sie in dem Ordner, der den Status „Fertig“ des Codelabs enthält.

Referenzdokumente und nächste Schritte