Geräteübergreifendes Firebase-Codelab

1. Einführung

Zuletzt aktualisiert: 14.03.2022

FlutterFire für die geräteübergreifende Kommunikation

Da immer mehr Geräte für die Heimautomatisierung, Wearables und Geräte für die persönliche Gesundheit online gehen, wird die geräteübergreifende Kommunikation zu einem immer wichtigeren Bestandteil der Entwicklung mobiler Anwendungen. Die Einrichtung der geräteübergreifenden Kommunikation, z. B. die Steuerung eines Browsers über eine Smartphone-App oder die Steuerung der Wiedergabe auf dem Fernseher über das Smartphone, ist in der Regel komplexer als die Entwicklung 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 sie mit dem Firebase Installations Service, um alle Geräte zu verfolgen und zu verbinden, auf denen sich derselbe Nutzer angemeldet hat. Sie verwenden Flutter, um schnell Anwendungen für mehrere Plattformen zu erstellen. Anschließend entwickeln 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:

  • 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
  • Mit diesen Apps können Nutzer die Musikwiedergabe auf einem Gerät über ein anderes Gerät steuern.

7f0279938e1d3ab5.gif

Lerninhalte

  • So erstellen und führen Sie eine Flutter-Musikplayer-App aus.
  • So ermöglichen Sie Nutzern, sich mit Firebase Auth anzumelden.
  • So verwenden Sie die Firebase RTDB Presence API und den Firebase Installation Service, um Geräte zu verbinden.

Voraussetzungen

  • Eine Flutter-Entwicklungsumgebung. Folgen Sie der Anleitung in der Flutter-Installationsanleitung, um sie einzurichten.
  • Es ist mindestens Flutter-Version 2.10 erforderlich. 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 erstellt. Der Startcode befindet sich in einem Git-Repository. Klonen Sie das Repository in der Befehlszeile, wechseln Sie in den Ordner mit dem Startzustand und installieren Sie die Abhängigkeiten:

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 App mit Ihrer bevorzugten IDE oder über die Befehlszeile erstellen.

Erstellen Sie im Anwendungsverzeichnis die App für das Web mit dem Befehl flutter run -d web-server.. Sie sollten den folgenden Prompt sehen.

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

Rufe http://localhost:<port> auf, um den Musikplayer zu sehen.

Wenn Sie mit dem Android-Emulator oder dem 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 Web-App sollte einen einfachen eigenständigen Musikplayer anzeigen. Prüfe, ob die Playerfunktionen wie vorgesehen funktionieren. Dies ist eine einfache Musik-Player-App, die für dieses Codelab entwickelt wurde. Es kann nur den Firebase-Song Better Together abspielen.

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 Overview > Use Simulator > Open and close a simulator (Simulatorübersicht > Simulator verwenden > Simulator öffnen und schließen).

3. Firebase einrichten

Firebase-Projekt erstellen

  1. Melden Sie sich mit Ihrem Google-Konto in der Firebase Console an.
  2. Klicken Sie auf die Schaltfläche, um ein neues Projekt zu erstellen, und geben Sie dann einen Projektnamen ein (z. B. Firebase-Cross-Device-Codelab).
  3. Klicken Sie auf Weiter.
  4. Lesen und akzeptieren Sie bei Aufforderung die Firebase-Nutzungsbedingungen und klicken Sie dann auf Weiter.
  5. (Optional) Aktivieren Sie die KI-Unterstützung in der Firebase Console (als „Gemini in Firebase“ bezeichnet).
  6. Für dieses Codelab benötigen Sie kein Google Analytics. Deaktivieren Sie daher die Google Analytics-Option.
  7. Klicken Sie auf Projekt erstellen, warten Sie, bis Ihr Projekt bereitgestellt wurde, und klicken Sie dann auf Weiter.

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 so, dass sie mindestens 1.13.1 ist, oder führen Sie flutter upgrade aus.

FlutterFire initialisieren

  1. Wenn Sie die Firebase CLI nicht installiert haben, können Sie sie mit dem Befehl curl -sL https://firebase.tools | bash installieren.
  2. Melden Sie sich an, indem Sie firebase login ausführen und der Anleitung folgen.
  3. Installieren Sie die FlutterFire CLI, indem Sie dart pub global activate flutterfire_cli ausführen.
  4. Konfigurieren Sie die FlutterFire CLI, indem Sie flutterfire configure ausführen.
  5. Wählen Sie an 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 Konfigurationsunterstützung auszuwählen.
  7. Wenn Sie nach der Apple-Bundle-ID gefragt werden, geben Sie eine eindeutige Domain ein oder com.example.appname. Das ist für dieses Codelab in Ordnung.

Nach der Konfiguration wird eine firebase_options.dart-Datei mit allen für die Initialisierung erforderlichen Optionen generiert.

Fügen Sie in Ihrem Editor den folgenden Code in die Datei „main.dart“ ein, 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, daher hat sich das Aussehen und Verhalten der App nicht geändert. Sie haben jetzt eine Firebase-App und können Firebase-Produkte verwenden, darunter:

  • Firebase Authentication, mit der sich Ihre Nutzer in Ihrer App anmelden können.
  • Firebase Realtime Database(RTDB): Sie verwenden die Presence API, um den Online-/Offlinestatus von Geräten zu verfolgen.
  • Mit Firebase-Sicherheitsregeln können Sie die Datenbank schützen.
  • Firebase Installations Service, um die Geräte zu identifizieren, auf denen sich ein einzelner Nutzer angemeldet hat.

4. Firebase Auth hinzufügen

E-Mail-Log-in für Firebase Authentication aktivieren

Damit Nutzer sich 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 Authentifizierung und dann auf die Schaltfläche Jetzt starten und auf den Tab Anmeldemethode.
  3. Klicken Sie in der Liste Anbieter für Anmeldungen auf E-Mail/Passwort, stellen Sie den Schalter Aktivieren auf „Ein“ und klicken Sie dann auf Speichern. 58e3e3e23c2f16a4.png

Firebase Authentication 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 Änderungen des App-Status wie An- und Abmeldungen zu verfolgen. Weitere Informationen dazu finden Sie in der Dokumentation Flutter state management.

Fügen Sie Folgendes in die neue application_state.dart-Datei 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, fügen Sie main.dart einen Initialisierungsschritt hinzu:

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 sich nicht geändert haben. Nutzer können sich jetzt jedoch anmelden und App-Status speichern.

Anmeldevorgang erstellen

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

  1. Ein abgemeldeter Nutzer startet den Anmeldevorgang, indem er in der App-Leiste rechts auf das Kontextmenü 71fcc1030a336423.png klickt.
  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. Wenn der Nutzer angemeldet ist, wird beim Klicken auf das Kontextmenü die Option Abmelden angezeigt.

c295f6fa2e1d40f3.png

Das Hinzufügen des Anmeldevorgangs erfordert drei Schritte.

Erstellen Sie zuerst ein AppBarMenuButton-Widget. Mit diesem Widget wird das Kontextmenü-Pop-up je nach loginState eines Nutzers gesteuert. 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 als Nächstes 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,
          ),
        ),
      ]),
    );
  }
}

Drittens: Suchen Sie das vorhandene appBar-Widget in main.dart. und fügen Sie AppBarMenuButton hinzu, um die Option Anmelden oder Abmelden anzuzeigen.

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. Das Kontextmenü 71fcc1030a336423.png sollte rechts in der App-Leiste angezeigt werden. Wenn Sie darauf klicken, wird ein Anmeldedialogfeld geöffnet.

Nachdem 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 über die 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. Rufen Sie in der Firebase Console den Bereich Realtime Database auf. 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. Sie fügen später Sicherheitsregeln hinzu. Wichtig: Sie sollten Ihre Sicherheitsregeln niemals im Testmodus in der Produktion verwenden.)

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

1b6076f60a36263b.png

Fügen Sie Ihre 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 nutzen Sie Firebase Installations und die Firebase RTDB Presence API, um eine Liste der Onlinegeräte eines einzelnen Nutzers zu verwalten. Der folgende Code hilft Ihnen dabei, dieses Ziel zu 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';
  }

Erstelle und führe die App über die Befehlszeile mit flutter run. auf deinem Gerät oder in einem Browser aus.

Melden Sie sich in Ihrer App als Nutzer an. Denken Sie daran, sich auf verschiedenen Plattformen mit demselben Nutzerkonto anzumelden.

In der Firebase Console sollten Ihre Geräte unter einer Nutzer-ID in Ihrer Datenbank angezeigt werden.

5bef49cea3564248.png

6. Gerätestatus synchronisieren

Hauptgerät auswählen

Wenn Sie Status zwischen Geräten synchronisieren möchten, legen Sie ein Gerät als Leader oder Controller fest. Das Mastergerät bestimmt die Status auf den Follower-Geräten.

Erstellen Sie eine setLeadDevice-Methode in application_state.dart und verfolgen 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;
      });
    }
  }

Um diese Funktion dem Kontextmenü der App-Leiste hinzuzufügen, erstellen Sie ein PopupMenuItem mit dem Namen Controller, indem Sie das SignedInMenuButton-Widget ändern. In diesem Menü können Nutzer das primäre Gerä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 Sie ein Lead-Gerät festgelegt haben, können Sie die Status des Lead-Geräts mit dem folgenden Code mit der RTDB synchronisieren. Hängen Sie den folgenden Code an das Ende von application_state.dart. an. Dadurch werden zwei Attribute gespeichert: der Playerstatus (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');
      }
    }
  }

Schließlich müssen Sie setActiveDeviceState immer dann aufrufen, wenn sich der Playerstatus 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 primären Geräts aus der Datenbank lesen

Es gibt zwei Teile, um den Status des Lead-Geräts zu lesen und zu verwenden. Richten Sie zuerst einen Datenbank-Listener für den Lead-Player-Status in application_state ein. Dieser Listener teilt den Follower-Geräten über einen Callback mit, wann der Bildschirm aktualisiert werden soll. In diesem Schritt haben Sie eine Schnittstelle OnLeadDeviceChangeCallback definiert. 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: Starten Sie den Datenbank-Listener während der Initialisierung des Players in player_widget.dart. Übergeben Sie die _updatePlayer-Funktion, damit der Status des Follower-Players 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 auf Emulatoren und/oder in einem Browser mit folgendem Befehl aus: flutter run -d <device-name>
  2. Öffnen Sie die Apps in einem Browser, auf einem iOS-Simulator oder einem Android-Emulator. Rufen Sie das Kontextmenü auf und wählen Sie eine App als Leader-Gerät aus. Die Player auf den Follower-Geräten sollten sich ändern, wenn das Leader-Gerät aktualisiert wird.
  3. Ändern Sie nun das Leader-Gerät, spielen Sie Musik ab oder pausieren Sie die Wiedergabe und beobachten Sie, wie die Follower-Geräte entsprechend aktualisiert werden.

Wenn die untergeordneten Geräte ordnungsgemäß aktualisiert werden, hast du erfolgreich einen geräteübergreifenden Controller erstellt. Es fehlt nur noch ein wichtiger Schritt.

7. Sicherheitsregeln aktualisieren

Wenn wir keine besseren Sicherheitsregeln schreiben, könnte jemand einen Status für ein Gerät schreiben, das ihm nicht gehört. Aktualisieren Sie daher vor dem Abschluss die Sicherheitsregeln für die Realtime Database, damit nur der Nutzer, der auf dem Gerät angemeldet ist, Daten auf dem Gerät lesen oder schreiben kann. Rufen Sie in der Firebase Console die Realtime Database und dann den Tab Regeln auf. Fügen Sie die folgenden Regeln ein, die es nur angemeldeten Nutzern erlauben, den Status ihrer eigenen Geräte zu lesen und zu schreiben:

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

8. Glückwunsch!

bcd986f7106d892b.gif

Herzlichen Glückwunsch! Sie haben mit Flutter erfolgreich eine geräteübergreifende Fernbedienung erstellt.

Gutschriften

Better Together – ein Firebase-Song

  • Musik von Ryan Vernon
  • Songtexte und Albumcover von Marissa Christy
  • Gesprochen von JP Gomez

9. Bonus

Als zusätzliche Herausforderung können Sie mit Flutter FutureBuilder den aktuellen Typ des primären Geräts asynchron in die Benutzeroberfläche einfügen. Wenn Sie Hilfe benötigen, finden Sie sie im Ordner mit dem fertigen Codelab.

Referenzdokumente und nächste Schritte