Firebase Cross Device Codelab

1 المقدمة

آخر تحديث: 2022-03-14

FlutterFire للاتصال عبر الأجهزة

نظرًا لأننا نشهد ظهور عدد كبير من أجهزة التشغيل الآلي للمنزل وأجهزة تكنولوجيا الصحة الشخصية القابلة للارتداء عبر الإنترنت، يصبح الاتصال عبر الأجهزة جزءًا متزايد الأهمية في بناء تطبيقات الهاتف المحمول. يعد إعداد الاتصال عبر الأجهزة، مثل التحكم في المتصفح من تطبيق الهاتف، أو التحكم في ما يتم تشغيله على التلفزيون من هاتفك، أكثر تعقيدًا من إنشاء تطبيق جوال عادي.

توفر قاعدة بيانات الوقت الفعلي الخاصة بـ Firebase واجهة برمجة التطبيقات للتواجد والتي تتيح للمستخدمين رؤية حالة أجهزتهم متصلة/غير متصلة بالإنترنت؛ ستستخدمه مع خدمة تثبيتات Firebase لتتبع جميع الأجهزة التي قام المستخدم نفسه بتسجيل الدخول إليها وتوصيلها. ستستخدم Flutter لإنشاء تطبيقات لمنصات متعددة بسرعة، ثم ستنشئ نموذجًا أوليًا عبر الأجهزة يمكن تشغيله الموسيقى على جهاز واحد والتحكم في الموسيقى على جهاز آخر!

ما سوف تبنيه

في هذا الدرس التطبيقي حول البرمجة، ستنشئ وحدة تحكم عن بعد بسيطة لمشغل الموسيقى. سيقوم تطبيقك بما يلي:

  • احصل على مشغل موسيقى بسيط على Android وiOS والويب، مصمم باستخدام Flutter.
  • السماح للمستخدمين بتسجيل الدخول.
  • قم بتوصيل الأجهزة عندما يقوم نفس المستخدم بتسجيل الدخول على أجهزة متعددة.
  • السماح للمستخدمين بالتحكم في تشغيل الموسيقى على جهاز واحد من جهاز آخر.

7f0279938e1d3ab5.gif

ما ستتعلمه

  • كيفية إنشاء وتشغيل تطبيق مشغل الموسيقى Flutter.
  • كيفية السماح للمستخدمين بتسجيل الدخول باستخدام Firebase Auth.
  • كيفية استخدام Firebase RTDB Presence API وخدمة تثبيت Firebase لتوصيل الأجهزة.

ماذا ستحتاج

  • بيئة تطوير الرفرفة. اتبع الإرشادات الموجودة في دليل تثبيت Flutter لإعداده.
  • مطلوب إصدار Flutter بحد أدنى 2.10 أو أعلى. إذا كان لديك إصدار أقل، فقم بتشغيل flutter upgrade.
  • حساب Firebase.

2. الإعداد

احصل على رمز البداية

لقد أنشأنا تطبيق مشغل موسيقى في Flutter. رمز البداية موجود في Git repo. للبدء، في سطر الأوامر، انسخ الريبو، وانتقل إلى المجلد الذي يحتوي على حالة البداية، وقم بتثبيت التبعيات:

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> .

يجب أن يعرض تطبيق الويب مشغل موسيقى أساسيًا مستقلاً. تأكد من أن ميزات المشغل تعمل على النحو المنشود. هذا هو تطبيق مشغل موسيقى بسيط مصمم لهذا الدرس التطبيقي للبرمجة. يمكنه فقط تشغيل أغنية Firebase، 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. إذا لم تكن واجهة سطر أوامر Firebase مثبتة لديك، فيمكنك تثبيتها عن طريق تشغيل curl -sL https://firebase.tools | bash .
  2. قم بتسجيل الدخول عن طريق تشغيل firebase login واتباع المطالبات.
  3. قم بتثبيت FlutterFire CLI عن طريق تشغيل dart pub global activate flutterfire_cli .
  4. قم بتكوين FlutterFire CLI عن طريق تشغيل flutterfire configure .
  5. عند المطالبة، اختر المشروع الذي أنشأته للتو لهذا الدرس التطبيقي حول التعليمات البرمجية، مثل Firebase-Cross-Device-Codelab .
  6. حدد iOS و Android و Web عندما يُطلب منك اختيار دعم التكوين.
  7. عند مطالبتك بمعرف حزمة Apple ، اكتب مجالًا فريدًا، أو أدخل com.example.appname ، وهو ما يناسب غرض هذا الدرس التطبيقي حول التعليمات البرمجية.

بمجرد التهيئة، سيتم إنشاء ملف 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 (RTDB) ؛ ستستخدم واجهة برمجة تطبيقات التواجد لتتبع حالة الجهاز متصل/غير متصل بالإنترنت
  • ستتيح لك قواعد أمان Firebase تأمين قاعدة البيانات.
  • خدمة تثبيتات Firebase لتحديد الأجهزة التي قام مستخدم واحد بتسجيل الدخول إليها.

4. أضف مصادقة Firebase

تمكين تسجيل الدخول عبر البريد الإلكتروني لمصادقة Firebase

للسماح للمستخدمين بتسجيل الدخول إلى تطبيق الويب، ستستخدم طريقة تسجيل الدخول بالبريد الإلكتروني/كلمة المرور :

  1. في وحدة تحكم Firebase، قم بتوسيع قائمة Build في اللوحة اليمنى.
  2. انقر فوق المصادقة ، ثم انقر فوق زر البدء ، ثم علامة التبويب طريقة تسجيل الدخول .
  3. انقر فوق البريد الإلكتروني/كلمة المرور في قائمة موفري تسجيل الدخول ، وقم بتعيين مفتاح التمكين على وضع التشغيل، ثم انقر فوق حفظ . 58e3e3e23c2f16a4.png

تكوين مصادقة Firebase في الرفرفة

في سطر الأوامر، قم بتشغيل الأوامر التالية لتثبيت حزم الرفرفة الضرورية:

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

ثالثًا، ابحث عن أداة 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. انتقل إلى قسم قاعدة بيانات الوقت الفعلي في وحدة تحكم 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 و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 Console، انتقل إلى Realtime Database، ثم إلى علامة التبويب Rules . الصق القواعد التالية التي تسمح فقط للمستخدم الذي قام بتسجيل الدخول بقراءة وكتابة حالات أجهزته الخاصة:

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

8. تهانينا!

bcd986f7106d892b.gif

تهانينا، لقد نجحت في إنشاء وحدة تحكم عن بعد عبر الأجهزة باستخدام Flutter!

الاعتمادات

أفضل معًا، أغنية Firebase

  • موسيقى ريان فيرنون
  • كلمات وغلاف الألبوم لماريسا كريستي
  • صوت جي بي جوميز

9. المكافأة

كتحدي إضافي، فكر في استخدام Flutter FutureBuilder لإضافة نوع الجهاز الرئيسي الحالي إلى واجهة المستخدم بشكل غير متزامن. إذا كنت بحاجة إلى مساعدة، فسيتم تنفيذها في المجلد الذي يحتوي على الحالة النهائية لبرنامج Codelab.

المستندات المرجعية والخطوات التالية