Laboratorium kodowania Firebase na różnych urządzeniach

1. Wstęp

Ostatnia aktualizacja: 14.03.2022

FlutterFire do komunikacji między urządzeniami

Ponieważ jesteśmy świadkami pojawienia się w Internecie dużej liczby urządzeń automatyki domowej, urządzeń do noszenia i osobistych technologii medycznych, komunikacja między urządzeniami staje się coraz ważniejszym elementem tworzenia aplikacji mobilnych. Konfigurowanie komunikacji na różnych urządzeniach, np. sterowanie przeglądarką za pomocą aplikacji na telefon lub kontrolowanie odtwarzania na telewizorze za pomocą telefonu, jest tradycyjnie bardziej złożone niż tworzenie zwykłej aplikacji mobilnej.

Baza danych czasu rzeczywistego Firebase udostępnia interfejs API obecności , który pozwala użytkownikom zobaczyć stan online/offline ich urządzenia; użyjesz go w połączeniu z usługą instalacyjną Firebase do śledzenia i łączenia wszystkich urządzeń, na których zalogował się ten sam użytkownik. Użyjesz Fluttera do szybkiego tworzenia aplikacji dla wielu platform, a następnie zbudujesz prototyp obsługujący wiele urządzeń, który będzie odtwarzał muzykę na jednym urządzeniu i steruj muzyką na innym!

Co zbudujesz

Podczas tych zajęć z programowania zbudujesz prosty pilot zdalnego sterowania odtwarzaczem muzyki. Twoja aplikacja:

  • Korzystaj z prostego odtwarzacza muzyki na Androida, iOS i przeglądarkę internetową, zbudowanego przy użyciu Flutter.
  • Zezwalaj użytkownikom na logowanie się.
  • Podłącz urządzenia, gdy ten sam użytkownik jest zalogowany na wielu urządzeniach.
  • Zezwalaj użytkownikom na kontrolowanie odtwarzania muzyki na jednym urządzeniu z innego urządzenia.

7f0279938e1d3ab5.gif

Czego się dowiesz

  • Jak zbudować i uruchomić aplikację odtwarzacza muzyki Flutter.
  • Jak zezwolić użytkownikom na logowanie się za pomocą Firebase Auth.
  • Jak używać Firebase RTDB Presence API i usługi instalacyjnej Firebase do łączenia urządzeń.

Co będziesz potrzebował

  • Środowisko programistyczne Flutter. Postępuj zgodnie z instrukcjami zawartymi w przewodniku instalacji Flutter, aby go skonfigurować.
  • Wymagana jest minimalna wersja Flutter 2.10 lub nowsza. Jeśli masz niższą wersję, uruchom flutter upgrade.
  • Konto Firebase.

2. Przygotowanie do pracy

Zdobądź kod startowy

Stworzyliśmy aplikację do odtwarzania muzyki w Flutter. Kod startowy znajduje się w repozytorium Git. Aby rozpocząć, w wierszu poleceń sklonuj repozytorium, przejdź do folderu ze stanem początkowym i zainstaluj zależności:

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

cd cross-device-controller/starter_code

flutter pub get

Zbuduj aplikację

Możesz pracować ze swoim ulubionym IDE, aby zbudować aplikację lub użyć wiersza poleceń.

W katalogu aplikacji zbuduj aplikację internetową za pomocą polecenia flutter run -d web-server. Powinieneś zobaczyć następujący monit.

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

Odwiedź http://localhost:<port> aby zobaczyć odtwarzacz muzyki.

Jeśli znasz emulator Androida lub symulator iOS, możesz zbudować aplikację dla tych platform i zainstalować ją za pomocą polecenia flutter run -d <device_name> .

Aplikacja internetowa powinna wyświetlać podstawowy, samodzielny odtwarzacz muzyki. Upewnij się, że funkcje odtwarzacza działają zgodnie z oczekiwaniami. Jest to prosta aplikacja do odtwarzania muzyki zaprojektowana dla tego laboratorium. Może odtwarzać tylko utwór Firebase, Better Together .

Skonfiguruj emulator Androida lub symulator iOS

Jeśli masz już urządzenie z Androidem lub iOS do programowania, możesz pominąć ten krok.

Aby utworzyć emulator Androida, pobierz Android Studio , które obsługuje również rozwój Flutter i postępuj zgodnie z instrukcjami w temacie Tworzenie i zarządzanie urządzeniami wirtualnymi .

Aby utworzyć symulator iOS, potrzebujesz środowiska Mac. Pobierz XCode i postępuj zgodnie z instrukcjami w temacie Przegląd symulatora > Użyj symulatora > Otwórz i zamknij symulator .

3. Konfigurowanie Firebase

Utwórz projekt Firebase

Otwórz przeglądarkę na http://console.firebase.google.com/ .

  1. Zaloguj się do Firebase .
  2. W konsoli Firebase kliknij Dodaj projekt (lub Utwórz projekt ) i nazwij swój projekt Firebase Firebase-Cross-Device-Codelab .
  3. Kliknij opcje tworzenia projektu. Jeśli pojawi się monit, zaakceptuj warunki Firebase. Pomiń konfigurowanie Google Analytics, ponieważ nie będziesz używać Analytics w tej aplikacji.

Nie musisz pobierać wspomnianych plików ani zmieniać plików build.gradle. Skonfigurujesz je podczas inicjowania FlutterFire.

Zainstaluj pakiet SDK Firebase

Z powrotem w wierszu poleceń, w katalogu projektu, uruchom następujące polecenie, aby zainstalować Firebase:

flutter pub add firebase_core

W pliku pubspec.yaml zmień wersję firebase_core na co najmniej 1.13.1 lub uruchom flutter upgrade

Zainicjuj FlutterFire

  1. Jeśli nie masz zainstalowanego interfejsu CLI Firebase, możesz go zainstalować, uruchamiając polecenie curl -sL https://firebase.tools | bash .
  2. Zaloguj się, uruchamiając firebase login i postępując zgodnie z instrukcjami.
  3. Zainstaluj interfejs CLI FlutterFire, uruchamiając dart pub global activate flutterfire_cli .
  4. Skonfiguruj interfejs CLI FlutterFire, uruchamiając flutterfire configure .
  5. Po wyświetleniu monitu wybierz projekt, który właśnie utworzyłeś dla tego ćwiczenia z programowania, na przykład Firebase-Cross-Device-Codelab .
  6. Po wyświetleniu monitu o wybranie obsługi konfiguracji wybierz iOS , Android i Internet .
  7. Po wyświetleniu monitu o podanie identyfikatora pakietu Apple wpisz unikalną domenę lub wpisz com.example.appname , co jest wystarczające na potrzeby tego ćwiczenia z programowania.

Po skonfigurowaniu zostanie wygenerowany plik firebase_options.dart zawierający wszystkie opcje wymagane do inicjalizacji.

W swoim edytorze dodaj następujący kod do pliku main.dart, aby zainicjować Flutter i 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());
}

Skompiluj aplikację za pomocą polecenia:

flutter run

Nie zmieniłeś jeszcze żadnych elementów interfejsu użytkownika, więc wygląd i zachowanie aplikacji nie uległy zmianie. Ale teraz masz aplikację Firebase i możesz zacząć korzystać z produktów Firebase, w tym:

  • Uwierzytelnianie Firebase , które umożliwia użytkownikom logowanie się do Twojej aplikacji.
  • Baza danych czasu rzeczywistego Firebase (RTDB) ; będziesz używać interfejsu API obecności do śledzenia stanu urządzenia w trybie online/offline
  • Reguły bezpieczeństwa Firebase pozwolą Ci zabezpieczyć bazę danych.
  • Usługa instalacji Firebase służąca do identyfikacji urządzeń, na których zalogował się pojedynczy użytkownik.

4. Dodaj uwierzytelnianie Firebase

Włącz logowanie e-mailem dla uwierzytelniania Firebase

Aby umożliwić użytkownikom logowanie się do aplikacji internetowej, użyj metody logowania za pomocą adresu e-mail/hasła :

  1. W konsoli Firebase rozwiń menu Kompilacja w lewym panelu.
  2. Kliknij opcję Uwierzytelnianie , a następnie kliknij przycisk Rozpocznij , a następnie kartę Metoda logowania .
  3. Kliknij opcję E-mail/hasło na liście dostawców logowania , ustaw przełącznik Włącz w pozycji włączonej, a następnie kliknij przycisk Zapisz . 58e3e3e23c2f16a4.png

Skonfiguruj uwierzytelnianie Firebase w Flutter

W wierszu poleceń uruchom następujące polecenia, aby zainstalować niezbędne pakiety flutter:

flutter pub add firebase_auth

flutter pub add provider

Dzięki tej konfiguracji możesz teraz utworzyć przepływ logowania i wylogowywania. Ponieważ stan uwierzytelniania nie powinien zmieniać się z ekranu na ekran, utworzysz klasę application_state.dart , aby śledzić zmiany stanu na poziomie aplikacji, takie jak logowanie i wylogowywanie. Więcej informacji na ten temat można znaleźć w dokumentacji zarządzania stanem Flutter .

Wklej następujący tekst do nowego pliku 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();
  }
}

Aby mieć pewność, że ApplicationState zostanie zainicjowany po uruchomieniu aplikacji, dodasz krok inicjalizacji do 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(),
  ));
}

Ponownie interfejs aplikacji powinien pozostać taki sam, ale teraz możesz pozwolić użytkownikom logować się i zapisywać stany aplikacji.

Utwórz przepływ logowania

Na tym etapie będziesz pracować nad procesem logowania i wylogowywania się. Oto jak będzie wyglądał przepływ:

  1. Zalogowany użytkownik zainicjuje proces logowania, klikając menu kontekstowe 71fcc1030a336423.png po prawej stronie paska aplikacji.
  2. Proces logowania zostanie wyświetlony w oknie dialogowym.
  3. Jeśli użytkownik nigdy wcześniej się nie logował, zostanie poproszony o utworzenie konta przy użyciu prawidłowego adresu e-mail i hasła.
  4. Jeśli użytkownik logował się wcześniej, zostanie poproszony o podanie hasła.
  5. Po zalogowaniu się użytkownika kliknięcie menu kontekstowego spowoduje wyświetlenie opcji Wyloguj się .

c295f6fa2e1d40f3.png

Dodanie przepływu logowania wymaga trzech kroków.

Przede wszystkim utwórz widżet AppBarMenuButton . Ten widget będzie sterował wyskakującym menu kontekstowym w zależności od loginState użytkownika. Dodaj import

lib/src/widgets.dart

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

Dołącz następujący kod do 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),
        ),
      ),
    ];
  }
}

Następnie w tej samej klasie widgets.dart utwórz widżet 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,
          ),
        ),
      ]),
    );
  }
}

Po trzecie, znajdź istniejący widget appBar w main.dart. Dodaj AppBarMenuButton , aby wyświetlić opcję Zaloguj się lub Wyloguj .

lib/main.dart

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

Uruchom polecenie flutter run , aby ponownie uruchomić aplikację z tymi zmianami. Powinieneś widzieć menu kontekstowe 71fcc1030a336423.png po prawej stronie paska aplikacji. Kliknięcie go przeniesie Cię do okna dialogowego logowania.

Po zalogowaniu się przy użyciu prawidłowego adresu e-mail i hasła powinna być widoczna opcja Wyloguj się w menu kontekstowym.

W konsoli Firebase w obszarze Uwierzytelnianie powinien być widoczny adres e-mail nowego użytkownika.

888506c86a28a72c.png

Gratulacje! Użytkownicy mogą teraz zalogować się do aplikacji!

5. Dodaj połączenie z bazą danych

Teraz możesz przejść do rejestracji urządzenia za pomocą interfejsu Firebase Presence API.

W wierszu poleceń uruchom następujące polecenia, aby dodać niezbędne zależności:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Utwórz bazę danych

W konsoli Firebase

  1. Przejdź do sekcji Baza danych czasu rzeczywistego w konsoli Firebase . Kliknij opcję Utwórz bazę danych .
  2. Jeśli pojawi się monit o wybranie trybu początkowego dla reguł bezpieczeństwa, wybierz na razie Tryb testowy **.** (Tryb testowy tworzy reguły bezpieczeństwa, które przepuszczają wszystkie żądania. Reguły bezpieczeństwa dodasz później. Ważne jest, aby nigdy nie przechodzić do wersji produkcyjnej z Twoje reguły bezpieczeństwa są nadal w trybie testowym.)

Baza danych jest na razie pusta. Znajdź adres URL swojej databaseURL w Ustawieniach projektu na karcie Ogólne . Przewiń w dół do sekcji Aplikacje internetowe .

1b6076f60a36263b.png

Dodaj databaseURL do pliku firebase_options.dart :

lib/firebase_options.dart

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

Zarejestruj urządzenia za pomocą interfejsu API obecności RTDB

Chcesz zarejestrować urządzenia użytkownika, gdy pojawią się online. Aby to zrobić, skorzystasz z Firebase Installations i Firebase RTDB Presence API, aby śledzić listę urządzeń online jednego użytkownika. Poniższy kod pomoże osiągnąć ten cel:

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

Wracając do wiersza poleceń, zbuduj i uruchom aplikację na swoim urządzeniu lub w przeglądarce z flutter run.

W aplikacji zaloguj się jako użytkownik. Pamiętaj, aby zalogować się jako ten sam użytkownik na różnych platformach.

W konsoli Firebase Twoje urządzenia powinny być widoczne w bazie danych pod jednym identyfikatorem użytkownika.

5bef49cea3564248.png

6. Synchronizuj stan urządzenia

Wybierz urządzenie wiodące

Aby zsynchronizować stany pomiędzy urządzeniami, wyznacz jedno urządzenie jako lidera, czyli kontrolera. Urządzenie wiodące będzie dyktować stany urządzeń uzupełniających.

Utwórz metodę setLeadDevice w application_state.dart i śledź to urządzenie za pomocą klucza active_device w 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;
      });
    }
  }

Aby dodać tę funkcję do menu kontekstowego paska aplikacji, utwórz element PopupMenuItem o nazwie Controller , modyfikując widżet SignedInMenuButton . To menu pozwoli użytkownikom ustawić urządzenie wiodące.

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

Zapisz stan urządzenia wiodącego do bazy danych

Po ustawieniu urządzenia wiodącego możesz zsynchronizować stany urządzenia wiodącego z RTDB za pomocą następującego kodu. Dołącz następujący kod na końcu pliku application_state.dart. Spowoduje to rozpoczęcie przechowywania dwóch atrybutów: stanu odtwarzacza (odtwarzanie lub pauza) i pozycji suwaka.

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

Na koniec musisz wywołać setActiveDeviceState za każdym razem, gdy stan odtwarzacza kontrolera zostanie zaktualizowany. Wprowadź następujące zmiany w istniejącym pliku player_widget.dart :

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

Odczytaj stan urządzenia wiodącego z bazy danych

Odczytywanie i używanie stanu urządzenia wiodącego składa się z dwóch części. Najpierw chcesz skonfigurować odbiornik bazy danych stanu głównego gracza w application_state . Ten słuchacz poinformuje urządzenia obserwujące, kiedy zaktualizować ekran za pomocą wywołania zwrotnego. Zwróć uwagę, że w tym kroku zdefiniowałeś interfejs OnLeadDeviceChangeCallback . Nie zostało to jeszcze zaimplementowane; w następnym kroku zaimplementujesz ten interfejs w 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();
      });
    }
  }

Po drugie, uruchom nasłuchiwanie bazy danych podczas inicjalizacji odtwarzacza w player_widget.dart . Przekaż funkcję _updatePlayer , aby stan odtwarzacza obserwującego mógł być aktualizowany za każdym razem, gdy zmieni się wartość bazy danych.

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

Teraz możesz przetestować aplikację:

  1. W wierszu poleceń uruchom aplikację na emulatorach i/lub w przeglądarce za pomocą: flutter run -d <device-name>
  2. Otwórz aplikacje w przeglądarce, na symulatorze iOS lub emulatorze Androida. Przejdź do menu kontekstowego, wybierz jedną aplikację, która będzie urządzeniem wiodącym. Powinieneś być w stanie zobaczyć, jak gracze na urządzeniach obserwujących zmieniają się wraz z aktualizacjami urządzenia lidera.
  3. Teraz zmień urządzenie prowadzące, odtwarzaj lub wstrzymuj muzykę i obserwuj odpowiednią aktualizację urządzeń obserwujących.

Jeśli urządzenia śledzące aktualizują się prawidłowo, udało Ci się stworzyć kontroler dla wielu urządzeń. Pozostał tylko jeden kluczowy krok.

7. Zaktualizuj reguły bezpieczeństwa

Jeśli nie napiszemy lepszych reguł bezpieczeństwa, ktoś mógłby zapisać stan na urządzeniu, którego nie jest właścicielem! Dlatego zanim skończysz, zaktualizuj Reguły zabezpieczeń bazy danych czasu rzeczywistego, aby upewnić się, że jedynymi użytkownikami, którzy mogą czytać i zapisywać na urządzeniu, są użytkownicy zalogowani na tym urządzeniu. W konsoli Firebase przejdź do Bazy danych czasu rzeczywistego, a następnie do karty Reguły . Wklej następujące reguły zezwalające tylko zalogowanemu użytkownikowi na odczytywanie i zapisywanie własnych stanów urządzenia:

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

8. Gratulacje!

bcd986f7106d892b.gif

Gratulacje, udało Ci się zbudować pilot zdalnego sterowania do wielu urządzeń za pomocą Flutter!

Kredyty

Better Together, piosenka Firebase

  • Muzyka Ryana Vernona
  • Teksty i okładka albumu są autorstwa Marissy Christy
  • Głos: JP Gomez

9. Bonus

Jako dodatkowe wyzwanie rozważ użycie Flutter FutureBuilder do asynchronicznego dodania bieżącego typu urządzenia wiodącego do interfejsu użytkownika. Jeśli potrzebujesz pomocy, jest ona zaimplementowana w folderze zawierającym ukończony stan laboratorium programistycznego.

Dokumenty referencyjne i kolejne kroki