Codelab בנושא Firebase במכשירים שונים

1. מבוא

העדכון האחרון: 14 במרץ 2022

‫FlutterFire לתקשורת בין מכשירים

ככל שיותר מכשירים של אוטומציה ביתית, מכשירים לבישים וטכנולוגיות בריאות אישיות מתחברים לאינטרנט, התקשורת בין מכשירים הופכת לחלק חשוב יותר ויותר בפיתוח אפליקציות לנייד. הגדרה של תקשורת בין מכשירים, כמו שליטה בדפדפן מאפליקציית טלפון או שליטה בהפעלה בטלוויזיה מהטלפון, היא בדרך כלל מורכבת יותר מאשר פיתוח של אפליקציה רגילה לנייד .

מסד הנתונים בזמן אמת של Firebase מספק את Presence API שמאפשר למשתמשים לראות את הסטטוס של המכשיר שלהם (מחובר או לא מחובר לאינטרנט). תשתמשו בו עם שירות ההתקנות של Firebase כדי לעקוב אחרי כל המכשירים שבהם אותו משתמש מחובר ולחבר אותם. תשתמשו ב-Flutter כדי ליצור במהירות אפליקציות למספר פלטפורמות, ואז תיצרו אב טיפוס חוצה מכשירים שמפעיל מוזיקה במכשיר אחד ושולט במוזיקה במכשיר אחר.

מה תפַתחו

בשיעור הזה תלמדו איך ליצור שלט פשוט לנגינת מוזיקה. האפליקציה שלכם:

  • יש נגן מוזיקה פשוט ב-Android, ב-iOS ובאינטרנט, שנבנה באמצעות Flutter.
  • המשתמשים יוכלו להיכנס לחשבון.
  • שיוך מכשירים כשאותו משתמש מחובר לכמה מכשירים.
  • אפליקציות שמאפשרות למשתמשים לשלוט בהשמעת מוזיקה במכשיר אחד ממכשיר אחר.

7f0279938e1d3ab5.gif

מה תלמדו

  • איך ליצור ולהפעיל אפליקציה של נגן מוזיקה ב-Flutter.
  • איך מאפשרים למשתמשים להיכנס באמצעות Firebase Auth.
  • איך משתמשים ב-Firebase RTDB Presence API וב-Firebase Installation Service כדי לחבר מכשירים.

מה צריך

  • סביבת פיתוח של Flutter. פועלים לפי ההוראות במדריך ההתקנה של Flutter כדי להגדיר אותו.
  • נדרשת גרסת Flutter מינימלית של 2.10 ואילך. אם יש לכם גרסה ישנה יותר, מריצים את flutter upgrade.
  • חשבון Firebase.

2. תהליך ההגדרה

קבלת קוד לתחילת הדרך

יצרנו אפליקציית נגן מוזיקה ב-Flutter. קוד ההתחלה נמצא במאגר Git. כדי להתחיל, בשורת הפקודה, משכפלים את מאגר הנתונים, עוברים לתיקייה עם מצב ההתחלה ומתקינים את התלות:

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

cd cross-device-controller/starter_code

flutter pub get

יצירת האפליקציה

אתם יכולים להשתמש בסביבת הפיתוח המשולבת (IDE) המועדפת עליכם כדי ליצור את האפליקציה, או להשתמש בשורת הפקודה.

בתיקיית האפליקציה, מריצים את הפקודה flutter run -d web-server. כדי ליצור את האפליקציה לאינטרנט. אמורה להופיע ההודעה הבאה.

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

כדי לראות את נגן המוזיקה, עוברים אל http://localhost:<port>.

אם אתם מכירים את האמולטור של Android או את הסימולטור של iOS, אתם יכולים ליצור את האפליקציה לפלטפורמות האלה ולהתקין אותה באמצעות הפקודה flutter run -d <device_name>.

אפליקציית האינטרנט אמורה להציג נגן מוזיקה בסיסי עצמאי. מוודאים שתכונות הנגן פועלות כמצופה. זוהי אפליקציית נגן מוזיקה פשוטה שנוצרה במיוחד ל-Codelab הזה. הוא יכול להשמיע רק שיר של Firebase, ‏ Better Together.

הגדרה של אמולטור Android או סימולטור iOS

אם כבר יש לכם מכשיר Android או מכשיר iOS לפיתוח, אתם יכולים לדלג על השלב הזה.

כדי ליצור אמולטור של Android, מורידים את Android Studio, שתומך גם בפיתוח של Flutter, ופועלים לפי ההוראות במאמר יצירה וניהול של מכשירים וירטואליים.

כדי ליצור סימולטור iOS, תצטרכו סביבת Mac. מורידים את XCode ופועלים לפי ההוראות במאמר סקירה כללית של Simulator > שימוש ב-Simulator > פתיחה וסגירה של סימולטור.

3. הגדרת Firebase

יצירת פרויקט Firebase

  1. נכנסים למסוף Firebase באמצעות חשבון Google.
  2. לוחצים על הלחצן כדי ליצור פרויקט חדש, ואז מזינים שם לפרויקט (לדוגמה, Firebase-Cross-Device-Codelab).
  3. לוחצים על המשך.
  4. אם מוצגת בקשה לעשות זאת, קוראים ומאשרים את התנאים של Firebase, ואז לוחצים על המשך.
  5. (אופציונלי) מפעילים את העזרה מבוססת-AI במסוף Firebase (שנקראת Gemini ב-Firebase).
  6. ב-codelab הזה לא צריך להשתמש ב-Google Analytics, ולכן משביתים את האפשרות Google Analytics.
  7. לוחצים על יצירת פרויקט, מחכים שהפרויקט יוקצה ולוחצים על המשך.

התקנת Firebase SDK

חוזרים לשורת הפקודה, בספריית הפרויקט, ומריצים את הפקודה הבאה כדי להתקין את Firebase:

flutter pub add firebase_core

בקובץ pubspec.yaml, עורכים את הגרסה של firebase_core כך שתהיה לפחות 1.13.1, או מריצים את הפקודה flutter upgrade

הפעלת FlutterFire

  1. אם לא התקנתם את Firebase CLI, אתם יכולים להתקין אותו על ידי הרצת הפקודה curl -sL https://firebase.tools | bash.
  2. מתחברים על ידי הפעלת הפקודה firebase login ופועלים לפי ההנחיות.
  3. מריצים את הפקודה dart pub global activate flutterfire_cli כדי להתקין את FlutterFire CLI.
  4. מגדירים את FlutterFire CLI על ידי הרצת הפקודה flutterfire configure.
  5. בהודעה שמופיעה, בוחרים את הפרויקט שיצרתם בשביל ה-codelab הזה, למשל Firebase-Cross-Device-Codelab.
  6. בוחרים באפשרות iOS, ‏ Android ואתר כשמוצגת בקשה לבחור תמיכה בהגדרה.
  7. כשמוצגת בקשה להזין את מזהה החבילה של Apple, מקלידים דומיין ייחודי או מזינים com.example.appname, וזה בסדר למטרת ה-Codelab הזה.

אחרי ההגדרה, ייווצר קובץ firebase_options.dart שמכיל את כל האפשרויות שנדרשות לאתחול.

בעורך, מוסיפים את הקוד הבא לקובץ main.dart כדי לאתחל את Flutter ואת 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());
}

קומפילציה של האפליקציה באמצעות הפקודה:

flutter run

עדיין לא שיניתם אף רכיב בממשק המשתמש, ולכן המראה וההתנהגות של האפליקציה לא השתנו. אבל עכשיו יש לכם אפליקציית Firebase, ואתם יכולים להתחיל להשתמש במוצרי Firebase, כולל:

  • אימות ב-Firebase, שמאפשר למשתמשים להיכנס לאפליקציה שלכם.
  • Firebase Realtime Database‏ (RTDB); תשתמשו ב-Presence API כדי לעקוב אחרי הסטטוס של המכשיר (מחובר או לא מחובר לאינטרנט)
  • כללי האבטחה של Firebase מאפשרים לאבטח את מסד הנתונים.
  • שירות ההתקנות של Firebase כדי לזהות את המכשירים שמשתמש יחיד נכנס אליהם.

4. הוספת Firebase Auth

הפעלת כניסה באמצעות אימייל ב-Firebase Authentication

כדי לאפשר למשתמשים להיכנס לאפליקציית האינטרנט, צריך להשתמש בשיטת הכניסה אימייל/סיסמה:

  1. ב-Firebase Console, מרחיבים את התפריט Build בחלונית הימנית.
  2. לוחצים על אימות, ואז על הלחצן תחילת העבודה, ואז על הכרטיסייה שיטת הכניסה.
  3. ברשימה ספקי כניסה, לוחצים על אימייל/סיסמה, מעבירים את המתג הפעלה למצב מופעל ואז לוחצים על שמירה. 58e3e3e23c2f16a4.png

הגדרת אימות ב-Firebase ב-Flutter

בשורת הפקודה, מריצים את הפקודות הבאות כדי להתקין את חבילות ה-Flutter הנדרשות:

flutter pub add firebase_auth

flutter pub add provider

אחרי שמגדירים את ההגדרה הזו, אפשר ליצור את תהליך הכניסה והיציאה. מכיוון שמצב האימות לא אמור להשתנות ממסך למסך, תיצרו מחלקה application_state.dart כדי לעקוב אחרי שינויים במצב ברמת האפליקציה, כמו כניסה ויציאה. מידע נוסף על הנושא הזה זמין במסמכי התיעוד בנושא ניהול מצב ב-Flutter.

מדביקים את הטקסט הבא לקובץ 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();
  }
}

כדי לוודא ש-ApplicationState יאותחל כשהאפליקציה תופעל, מוסיפים שלב אתחול ל-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(),
  ));
}

שוב, ממשק המשתמש של האפליקציה אמור להישאר זהה, אבל עכשיו אפשר לאפשר למשתמשים להיכנס ולשמור את מצבי האפליקציה.

יצירת תהליך כניסה

בשלב הזה תעבדו על תהליך הכניסה והיציאה. כך ייראה התהליך:

  1. משתמש שלא מחובר יתחיל את תהליך הכניסה בלחיצה על תפריט ההקשר 71fcc1030a336423.pngבצד שמאל של סרגל האפליקציות.
  2. תהליך הכניסה יוצג בתיבת דו-שיח.
  3. אם המשתמש מעולם לא נכנס לחשבון, הוא יתבקש ליצור חשבון באמצעות כתובת אימייל וסיסמה תקינות.
  4. אם המשתמש כבר נכנס לחשבון בעבר, הוא יתבקש להזין את הסיסמה שלו.
  5. אחרי שהמשתמש מתחבר, לחיצה על תפריט ההקשר תציג את האפשרות יציאה.

c295f6fa2e1d40f3.png

הוספת תהליך כניסה לחשבון מתבצעת בשלושה שלבים.

קודם כול, יוצרים ווידג'ט AppBarMenuButton. הווידג'ט הזה ישלוט בחלון הקופץ של תפריט ההקשר בהתאם ל-loginState של המשתמש. הוספת הייבוא

lib/src/widgets.dart

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

מוסיפים את הקוד הבא אל 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),
        ),
      ),
    ];
  }
}

בשלב השני, באותה כיתה widgets.dart, יוצרים את הווידג'ט 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,
          ),
        ),
      ]),
    );
  }
}

בשלב השלישי, מוצאים את הווידג'ט הקיים של סרגל האפליקציות ב-main.dart. מוסיפים את AppBarMenuButton כדי להציג את האפשרות כניסה או יציאה.

lib/main.dart

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

מריצים את הפקודה flutter run כדי להפעיל מחדש את האפליקציה עם השינויים האלה. תפריט ההקשר 71fcc1030a336423.png אמור להופיע בצד שמאל של סרגל האפליקציות. לחיצה עליו תפתח תיבת דו-שיח לכניסה לחשבון.

אחרי שתיכנסו לחשבון עם כתובת אימייל וסיסמה תקינות, אמורה להופיע בתפריט ההקשר האפשרות יציאה מהחשבון.

במסוף Firebase, בקטע אימות, כתובת האימייל אמורה להופיע כמשתמש חדש.

888506c86a28a72c.png

כל הכבוד! המשתמשים יכולים עכשיו להיכנס לאפליקציה.

5. הוספת חיבור למסד נתונים

עכשיו אפשר להמשיך לרישום המכשיר באמצעות Firebase Presence API.

בשורת הפקודה, מריצים את הפקודות הבאות כדי להוסיף את יחסי התלות הנדרשים:

flutter pub add firebase_app_installations

flutter pub add firebase_database

יצירת מסד נתונים

במסוף Firebase,

  1. עוברים לקטע Realtime Database במסוף Firebase. לוחצים על יצירת מסד נתונים.
  2. אם מוצגת בקשה לבחור מצב התחלתי לכללי האבטחה, בוחרים באפשרות מצב בדיקה לעת עתה**.** (מצב הבדיקה יוצר כללי אבטחה שמאפשרים לכל הבקשות לעבור. תוכלו להוסיף כללי אבטחה בהמשך. חשוב לא להעביר אף פעם את כללי האבטחה למצב ייצור כשהם עדיין במצב בדיקה).

מסד הנתונים ריק כרגע. אפשר למצוא את databaseURL בהגדרות הפרויקט, בכרטיסייה כללי. גוללים למטה לקטע אפליקציות אינטרנט.

1b6076f60a36263b.png

הוספת databaseURL לקובץ firebase_options.dart:

lib/firebase_options.dart

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

רישום מכשירים באמצעות RTDB Presence API

אתם רוצים לרשום את המכשירים של המשתמש כשהם מופיעים אונליין. כדי לעשות את זה, תשתמשו ב-Firebase Installations וב-Firebase RTDB Presence API כדי לעקוב אחרי רשימה של מכשירים שמחוברים לאינטרנט של משתמש יחיד. הקוד הבא יעזור להשיג את המטרה הזו:

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

חוזרים לשורת הפקודה, יוצרים את האפליקציה ומריצים אותה במכשיר או בדפדפן באמצעות flutter run.

באפליקציה, נכנסים לחשבון כמשתמש. חשוב לזכור להיכנס לחשבון באותו שם משתמש בפלטפורמות שונות.

במסוף Firebase, המכשירים שלכם אמורים להופיע תחת מזהה משתמש אחד במסד הנתונים.

5bef49cea3564248.png

6. סנכרון סטטוס המכשיר

בחירת מכשיר מוביל

כדי לסנכרן את המצבים בין המכשירים, צריך להגדיר מכשיר אחד כמכשיר הראשי או כמכשיר הבקרה. המכשיר הראשי יקבע את המצבים במכשירים המשניים.

יוצרים שיטת setLeadDevice ב-application_state.dart ועוקבים אחרי המכשיר הזה באמצעות המפתח active_device ב-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;
      });
    }
  }

כדי להוסיף את הפונקציונליות הזו לתפריט ההקשר של סרגל האפליקציות, צריך ליצור PopupMenuItem בשם Controller על ידי שינוי הווידג'ט SignedInMenuButton. בתפריט הזה המשתמשים יכולים להגדיר את המכשיר הראשי.

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

כתיבת המצב של המכשיר הראשי למסד הנתונים

אחרי שמגדירים מכשיר ראשי, אפשר לסנכרן את המצבים של המכשיר הראשי עם RTDB באמצעות הקוד הבא. מוסיפים את הקוד הבא לסוף application_state.dart.הפעולה הזו תתחיל לאחסן שני מאפיינים: מצב הנגן (הפעלה או השהיה) ומיקום פס ההזזה.

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

לבסוף, צריך להפעיל את setActiveDeviceState בכל פעם שמצב הנגן של בקר המשחקים מתעדכן. מבצעים את השינויים הבאים בקובץ 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;
  }

קריאת מצב המכשיר הראשי ממסד הנתונים

יש שני חלקים לקריאה ולשימוש במצב של המכשיר הראשי. קודם כל, צריך להגדיר מאזין למסד נתונים של מצב הנגן הראשי ב-application_state. המאזין הזה יגיד למכשירים העוקבים מתי לעדכן את המסך באמצעות קריאה חוזרת. שימו לב שהגדרתם ממשק OnLeadDeviceChangeCallback בשלב הזה. הוא עדיין לא הוטמע. בשלב הבא תטמיעו את הממשק הזה ב-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();
      });
    }
  }

בשלב השני, מפעילים את מאזין מסד הנתונים במהלך אתחול הנגן ב-player_widget.dart. מעבירים את הפונקציה _updatePlayer כדי שמצב הנגן של העוקב יוכל להתעדכן בכל פעם שהערך במסד הנתונים משתנה.

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

עכשיו אפשר לבדוק את האפליקציה:

  1. בשורת הפקודה, מריצים את האפליקציה באמולטורים ו/או בדפדפן באמצעות: flutter run -d <device-name>
  2. פותחים את האפליקציות בדפדפן, בסימולטור iOS או באמולטור Android. עוברים לתפריט ההקשר ובוחרים אפליקציה אחת שתהיה המכשיר הראשי. אמורה להיות לכם אפשרות לראות את השינויים בנגנים של המכשירים העוקבים כשהמכשיר המוביל מתעדכן.
  3. עכשיו משנים את המכשיר הראשי, מפעילים או מפסיקים את המוזיקה ורואים שהמכשירים המשניים מתעדכנים בהתאם.

אם המכשירים המשניים מתעדכנים כמו שצריך, סימן שהצלחתם ליצור בקר חוצה מכשירים. נותר רק עוד שלב אחד חשוב.

7. עדכון כללי האבטחה

אם לא נכתוב כללי אבטחה טובים יותר, מישהו יוכל לכתוב מצב למכשיר שלא בבעלותו. לכן, לפני שמסיימים, צריך לעדכן את כללי האבטחה של מסד הנתונים בזמן אמת כדי לוודא שרק המשתמש שמחובר למכשיר יכול לקרוא או לכתוב בו. במסוף Firebase, עוברים אל Realtime Database ואז אל הכרטיסייה Rules (כללים). מדביקים את הכללים הבאים שמאפשרים רק למשתמשים מחוברים לקרוא ולכתוב את מצבי המכשירים שלהם:

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

8. כל הכבוד!

bcd986f7106d892b.gif

כל הכבוד, יצרתם בהצלחה שלט רחוק שפועל בכמה מכשירים באמצעות Flutter!

זיכויים

Better Together, a Firebase Song

  • מוזיקה מאת Ryan Vernon
  • מילים ושער אלבום מאת Marissa Christy
  • קריינות: JP Gomez

9. בונוס

כדי להוסיף אתגר, אפשר להשתמש ב-Flutter FutureBuilder כדי להוסיף את סוג המכשיר הנוכחי של הלידים לממשק המשתמש באופן אסינכרוני. אם אתם צריכים עזרה, אפשר למצוא אותה בתיקייה שמכילה את המצב הסופי של ה-codelab.

מסמכי עזר והשלבים הבאים