درس تطبيقي حول الترميز على جميع الأجهزة في Firebase

1. مقدمة

تاريخ آخر تعديل: 2022-03-14

‫FlutterFire للتواصل بين الأجهزة

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

توفّر قاعدة البيانات في الوقت الفعلي من Firebase واجهة برمجة التطبيقات Presence API التي تتيح للمستخدمين الاطّلاع على حالة أجهزتهم على الإنترنت أو عدم الاتصال بالإنترنت. ويمكنك استخدامها مع خدمة "عمليات تثبيت Firebase" لتتبُّع جميع الأجهزة التي سجّل المستخدم نفسه الدخول إليها وربطها. ستستخدم Flutter لإنشاء تطبيقات بسرعة على منصات متعددة، ثم ستنشئ نموذجًا أوليًا يعمل على أجهزة متعددة ويشغّل الموسيقى على أحد الأجهزة ويتحكّم فيها على جهاز آخر.

ما ستنشئه

في هذا الدرس العملي، ستنشئ جهاز تحكّم بسيطًا عن بُعد لمشغّل موسيقى. سيتم إجراء ما يلي في تطبيقك:

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

7f0279938e1d3ab5.gif

ما ستتعلمه

  • كيفية إنشاء تطبيق مشغّل موسيقى متوافق مع Flutter وتشغيله
  • كيفية السماح للمستخدمين بتسجيل الدخول باستخدام Firebase Auth
  • كيفية استخدام واجهة برمجة التطبيقات Presence API في قاعدة بيانات Firebase في الوقت الفعلي وخدمة Firebase Installation Service لربط الأجهزة

المتطلبات

  • بيئة تطوير 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

إنشاء التطبيق

يمكنك استخدام بيئة التطوير المتكاملة المفضّلة لديك لإنشاء التطبيق، أو استخدام سطر الأوامر.

في دليل تطبيقك، أنشئ التطبيق للويب باستخدام الأمر 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 الذي يتيح أيضًا تطوير Flutter، واتّبِع التعليمات الواردة في إنشاء الأجهزة الافتراضية وإدارتها.

لإنشاء محاكي iOS، يجب توفّر بيئة Mac. نزِّل XCode واتّبِع التعليمات الواردة في نظرة عامة على المحاكي > استخدام المحاكي > فتح محاكي وإغلاقه.

3- إعداد Firebase

إنشاء مشروع على Firebase

  1. سجِّل الدخول إلى وحدة تحكّم Firebase باستخدام حسابك على Google.
  2. انقر على الزر لإنشاء مشروع جديد، ثم أدخِل اسم المشروع (على سبيل المثال، Firebase-Cross-Device-Codelab).
  3. انقر على متابعة.
  4. إذا طُلب منك ذلك، راجِع بنود Firebase واقبلها، ثم انقر على متابعة.
  5. (اختياري) فعِّل ميزة "المساعدة المستندة إلى الذكاء الاصطناعي" في وحدة تحكّم Firebase (المعروفة باسم "Gemini في Firebase").
  6. في هذا الدرس العملي، لا تحتاج إلى "إحصاءات Google"، لذا أوقِف خيار "إحصاءات Google".
  7. انقر على إنشاء مشروع، وانتظِر إلى أن يتم توفير مشروعك، ثم انقر على متابعة.

تثبيت حزمة تطوير البرامج (SDK) لمنصة Firebase

في سطر الأوامر، ضِمن دليل المشروع، نفِّذ الأمر التالي لتثبيت 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 والويب عندما يُطلب منك اختيار دعم الإعداد.
  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 في الوقت الفعلي(RTDB): ستستخدم واجهة برمجة التطبيقات الخاصة بحالة الاتصال لتتبُّع حالة الجهاز على الإنترنت أو عدم الاتصال به
  • ستتيح لك قواعد الأمان في Firebase تأمين قاعدة البيانات.
  • خدمة "عمليات تثبيت Firebase" لتحديد الأجهزة التي سجّل مستخدم واحد الدخول إليها

4. إضافة Firebase Auth

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

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

  1. في وحدة تحكّم Firebase، وسِّع قائمة إنشاء في اللوحة اليمنى.
  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,
          ),
        ),
      ]),
    );
  }
}

ثالثًا، ابحث عن أداة 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" وواجهة برمجة التطبيقات Presence في قاعدة بيانات Firebase في الوقت الفعلي لتتبُّع قائمة بالأجهزة المتصلة بالإنترنت من مستخدم واحد. سيساعد الرمز التالي في تحقيق هذا الهدف:

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. تعديل قواعد الأمان

ما لم نكتب قواعد أمان أفضل، يمكن لشخص ما كتابة حالة لجهاز لا يملكه. لذا، قبل الانتهاء، عدِّل "قواعد أمان Realtime Database" للتأكّد من أنّ المستخدم الوحيد الذي يمكنه قراءة البيانات أو كتابتها على جهاز هو المستخدم الذي سجّل الدخول إلى هذا الجهاز. في وحدة تحكّم Firebase، انتقِل إلى Realtime Database، ثم إلى علامة التبويب القواعد. ألصِق القواعد التالية التي تسمح للمستخدم الذي سجّل الدخول فقط بقراءة حالات أجهزته وكتابتها:

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

8. تهانينا!

bcd986f7106d892b.gif

تهانينا، لقد أنشأت وحدة تحكّم عن بُعد متوافقة مع أجهزة متعددة باستخدام Flutter بنجاح.

الساعات المعتمَدة

Better Together, a Firebase Song

  • موسيقى من تأليف "ريان فيرنون"
  • كلمات الأغاني وغلاف الألبوم من تصميم "ماريسا كريستي"
  • أداء صوتي: JP Gomez

9. مكافأة

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

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