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

1. מבוא

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

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

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

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

מה תפַתחו

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

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

7f0279938e1d3ab5.gif

מה תלמדו

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

מה צריך להכין

  • סביבת פיתוח של Flutter. פועלים לפי ההוראות במדריך להתקנתFlutter כדי להגדיר אותו.
  • נדרשת גרסה 2.10 ומעלה של Flutter. אם הגרסה שלך ישנה יותר, מריצים את 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 הזה. אפשר להפעיל בה רק את השיר Better Together.

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

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

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

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

3. הגדרת Firebase

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

פותחים דפדפן בכתובת http://console.firebase.google.com/.

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

אין צורך להוריד את הקבצים שצוינו או לשנות את קובצי build.gradle. תגדירו אותם כשמאתחלים את FlutterFire.

התקנת Firebase SDK

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

flutter pub add firebase_core

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

הפעלת FlutterFire

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

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

כדי לאתחל את Flutter ו-Firebase, בעורך שלך, יש להוסיף את הקוד הבא לקובץ Main.dart:

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‏ (RTDB): תשתמשו ב-API של נוכחות כדי לעקוב אחרי סטטוס המכשיר אופליין/אונליין
  • כללי אבטחה של Firebase יאפשרו לכם לאבטח את מסד הנתונים.
  • שירות התקנות של Firebase כדי לזהות את המכשירים שבהם משתמש אחד נכנס לחשבון.

4. הוספת אימות מ-Firebase

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

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

  1. במסוף Firebase, מרחיבים את התפריט Build בחלונית הימנית.
  2. לוחצים על Authentication (אימות) ואז לוחצים על הלחצן Get Started (תחילת העבודה) ואז על הכרטיסייה Sign-in method (שיטת הכניסה).
  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),
        ),
      ),
    ];
  }
}

בשלב השני, יוצרים את הווידג'ט SignInDialog באותה כיתה widgets.dart.

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

בשלב השלישי, מאתרים את הווידג'ט הקיים של appBar ב-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. לוחצים על Create Database.
  2. אם מוצגת בקשה לבחור מצב התחלה לכללי האבטחה, צריך לבחור באפשרות מצב בדיקה בינתיים**.** (במצב בדיקה נוצרים כללי אבטחה שמאפשרים את כל הבקשות. כללי אבטחה יתווספו בהמשך. חשוב לעולם לא לעבור לסביבת הייצור כשכללי האבטחה עדיין נמצאים במצב בדיקה).

מסד הנתונים ריק כרגע. מאתרים את databaseURL בקטע Project settings (הגדרות הפרויקט), בכרטיסייה General (כללי). גוללים למטה לקטע אפליקציות אינטרנט.

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

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

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

8. כל הכבוד!

bcd986f7106d892b.gif

מזל טוב, יצרתם שלט רחוק למכשירים שונים באמצעות Flutter!

זיכויים

Better Together, שיר של Firebase

  • מוזיקה של ריאן ורנון
  • מילות השיר ועטיפת האלבום מאת Marissa Christy
  • קול של JP גומז

9. בונוס

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

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