1. مقدمة
تاريخ آخر تعديل: 2022-03-14
FlutterFire للتواصل بين الأجهزة
مع تزايد عدد أجهزة التشغيل الآلي للمنزل والأجهزة القابلة للارتداء وأجهزة تكنولوجيا الصحة الشخصية التي تتصل بالإنترنت، أصبح التواصل بين الأجهزة جزءًا مهمًا بشكل متزايد من إنشاء تطبيقات الأجهزة الجوّالة. إنّ إعداد ميزة التواصل بين الأجهزة، مثل التحكّم في متصفّح من تطبيق على الهاتف أو التحكّم في المحتوى الذي يتم تشغيله على التلفزيون من الهاتف، يكون عادةً أكثر تعقيدًا من إنشاء تطبيق عادي على الأجهزة الجوّالة .
توفّر قاعدة البيانات في الوقت الفعلي من Firebase واجهة برمجة التطبيقات Presence API التي تتيح للمستخدمين الاطّلاع على حالة أجهزتهم على الإنترنت أو عدم الاتصال بالإنترنت. ويمكنك استخدامها مع خدمة "عمليات تثبيت Firebase" لتتبُّع جميع الأجهزة التي سجّل المستخدم نفسه الدخول إليها وربطها. ستستخدم Flutter لإنشاء تطبيقات بسرعة على منصات متعددة، ثم ستنشئ نموذجًا أوليًا يعمل على أجهزة متعددة ويشغّل الموسيقى على أحد الأجهزة ويتحكّم فيها على جهاز آخر.
ما ستنشئه
في هذا الدرس العملي، ستنشئ جهاز تحكّم بسيطًا عن بُعد لمشغّل موسيقى. سيتم إجراء ما يلي في تطبيقك:
- تطبيق بسيط لتشغيل الموسيقى على Android وiOS والويب، تم إنشاؤه باستخدام Flutter.
- السماح للمستخدمين بتسجيل الدخول
- ربط الأجهزة عندما يسجّل المستخدم نفسه الدخول على أجهزة متعدّدة
- هي فئة تطبيقات تسمح للمستخدمين بالتحكّم في تشغيل الموسيقى على أحد الأجهزة من جهاز آخر.
ما ستتعلمه
- كيفية إنشاء تطبيق مشغّل موسيقى متوافق مع 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
- سجِّل الدخول إلى وحدة تحكّم Firebase باستخدام حسابك على Google.
- انقر على الزر لإنشاء مشروع جديد، ثم أدخِل اسم المشروع (على سبيل المثال،
Firebase-Cross-Device-Codelab
).
- انقر على متابعة.
- إذا طُلب منك ذلك، راجِع بنود Firebase واقبلها، ثم انقر على متابعة.
- (اختياري) فعِّل ميزة "المساعدة المستندة إلى الذكاء الاصطناعي" في وحدة تحكّم Firebase (المعروفة باسم "Gemini في Firebase").
- في هذا الدرس العملي، لا تحتاج إلى "إحصاءات Google"، لذا أوقِف خيار "إحصاءات Google".
- انقر على إنشاء مشروع، وانتظِر إلى أن يتم توفير مشروعك، ثم انقر على متابعة.
تثبيت حزمة تطوير البرامج (SDK) لمنصة Firebase
في سطر الأوامر، ضِمن دليل المشروع، نفِّذ الأمر التالي لتثبيت Firebase:
flutter pub add firebase_core
في ملف pubspec.yaml
، عدِّل إصدار firebase_core
ليكون 1.13.1 على الأقل، أو شغِّل flutter upgrade
إعداد FlutterFire
- إذا لم يكن لديك واجهة سطر الأوامر في Firebase مثبّتة، يمكنك تثبيتها عن طريق تنفيذ
curl -sL https://firebase.tools | bash
. - سجِّل الدخول عن طريق تنفيذ
firebase login
واتّباع التعليمات. - ثبِّت FlutterFire CLI عن طريق تنفيذ
dart pub global activate flutterfire_cli
. - اضبط FlutterFire CLI من خلال تنفيذ
flutterfire configure
. - عند ظهور الطلب، اختَر المشروع الذي أنشأته للتو لهذا الدرس العملي، مثل Firebase-Cross-Device-Codelab.
- اختَر iOS وAndroid والويب عندما يُطلب منك اختيار دعم الإعداد.
- عندما يُطلب منك إدخال معرّف حزمة 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"
للسماح للمستخدمين بتسجيل الدخول إلى تطبيق الويب، عليك استخدام طريقة تسجيل الدخول البريد الإلكتروني/كلمة المرور:
- في وحدة تحكّم Firebase، وسِّع قائمة إنشاء في اللوحة اليمنى.
- انقر على المصادقة، ثم على الزر البدء، ثم على علامة التبويب طريقة تسجيل الدخول.
- انقر على البريد الإلكتروني/كلمة المرور في قائمة مزوّدو خدمة تسجيل الدخول، واضبط مفتاح تفعيل على وضع التشغيل، ثم انقر على حفظ.
ضبط "مصادقة 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(),
));
}
مرة أخرى، يجب أن تظل واجهة مستخدم التطبيق كما هي، ولكن يمكنك الآن السماح للمستخدمين بتسجيل الدخول وحفظ حالات التطبيق.
إنشاء مسار تسجيل الدخول
في هذه الخطوة، ستعمل على عملية تسجيل الدخول والخروج. في ما يلي الشكل الذي سيبدو عليه مسار العمل:
- يبدأ المستخدم الذي سجّل خروجه عملية تسجيل الدخول من خلال النقر على قائمة السياق
على يسار شريط التطبيق.
- سيتم عرض عملية تسجيل الدخول في مربّع حوار.
- إذا لم يسبق للمستخدم تسجيل الدخول، سيُطلب منه إنشاء حساب باستخدام عنوان بريد إلكتروني صالح وكلمة مرور.
- إذا سبق للمستخدم تسجيل الدخول، سيُطلب منه إدخال كلمة المرور.
- بعد تسجيل الدخول، سيؤدي النقر على قائمة السياق إلى عرض خيار تسجيل الخروج.
تتطلّب إضافة مسار تسجيل الدخول ثلاث خطوات.
أولاً، أنشئ 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
لإعادة تشغيل التطبيق مع هذه التغييرات. من المفترض أن تظهر قائمة السياق على الجانب الأيسر من شريط التطبيق. سيؤدي النقر عليه إلى فتح مربّع حوار تسجيل الدخول.
بعد تسجيل الدخول باستخدام عنوان بريد إلكتروني صالح وكلمة مرور، من المفترض أن يظهر لك خيار تسجيل الخروج في قائمة السياق.
في وحدة تحكُّم Firebase، ضِمن المصادقة، من المفترض أن يظهر لك عنوان البريد الإلكتروني مُدرَجًا كمستخدم جديد.
تهانينا! يمكن للمستخدمين الآن تسجيل الدخول إلى التطبيق.
5- إضافة اتصال بقاعدة بيانات
أنت الآن مستعد للانتقال إلى تسجيل الجهاز باستخدام واجهة برمجة التطبيقات Firebase Presence API.
في سطر الأوامر، نفِّذ الأوامر التالية لإضافة التبعيات اللازمة:
flutter pub add firebase_app_installations
flutter pub add firebase_database
إنشاء قاعدة بيانات
في وحدة تحكّم Firebase،
- انتقِل إلى قسم قاعدة البيانات في الوقت الفعلي في وحدة تحكّم Firebase. انقر على إنشاء قاعدة بيانات.
- إذا طُلب منك اختيار وضع بدء لقواعد الأمان، اختَر وضع الاختبار في الوقت الحالي**.** (ينشئ "وضع الاختبار" قواعد أمان تسمح بجميع الطلبات. ستضيف قواعد الأمان لاحقًا. من المهم عدم الانتقال إلى مرحلة الإنتاج مطلقًا إذا كانت "قواعد الأمان" لا تزال في "وضع الاختبار").
قاعدة البيانات فارغة في الوقت الحالي. ابحث عن databaseURL
في إعدادات المشروع ضمن علامة التبويب عام. انتقِل للأسفل إلى قسم تطبيقات الويب.
إضافة 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، من المفترض أن تظهر أجهزتك ضمن رقم تعريف مستخدم واحد في قاعدة البيانات.
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');
}
}
}
}
أنت الآن جاهز لاختبار التطبيق:
- في سطر الأوامر، شغِّل التطبيق على المحاكيات و/أو في المتصفّح باستخدام:
flutter run -d <device-name>
- افتح التطبيقات في متصفّح أو على محاكي iOS أو محاكي Android. انتقِل إلى قائمة السياق، واختَر تطبيقًا واحدًا ليكون الجهاز الرئيسي. من المفترض أن تلاحظ تغيُّر مشغّلات الأجهزة التابعة عند تحديث الجهاز الرئيسي.
- يمكنك الآن تغيير الجهاز الرئيسي وتشغيل الموسيقى أو إيقافها مؤقتًا، وملاحظة أنّ الأجهزة الثانوية يتم تعديلها وفقًا لذلك.
إذا تم تحديث الأجهزة التابعة بشكل صحيح، تكون قد نجحت في إنشاء وحدة تحكّم على أجهزة متعددة. لم يتبقَّ سوى خطوة واحدة مهمة.
7. تعديل قواعد الأمان
ما لم نكتب قواعد أمان أفضل، يمكن لشخص ما كتابة حالة لجهاز لا يملكه. لذا، قبل الانتهاء، عدِّل "قواعد أمان Realtime Database" للتأكّد من أنّ المستخدم الوحيد الذي يمكنه قراءة البيانات أو كتابتها على جهاز هو المستخدم الذي سجّل الدخول إلى هذا الجهاز. في وحدة تحكّم Firebase، انتقِل إلى Realtime Database، ثم إلى علامة التبويب القواعد. ألصِق القواعد التالية التي تسمح للمستخدم الذي سجّل الدخول فقط بقراءة حالات أجهزته وكتابتها:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. تهانينا!
تهانينا، لقد أنشأت وحدة تحكّم عن بُعد متوافقة مع أجهزة متعددة باستخدام Flutter بنجاح.
الساعات المعتمَدة
Better Together, a Firebase Song
- موسيقى من تأليف "ريان فيرنون"
- كلمات الأغاني وغلاف الألبوم من تصميم "ماريسا كريستي"
- أداء صوتي: JP Gomez
9. مكافأة
كتحدٍ إضافي، يمكنك استخدام Flutter FutureBuilder
لإضافة نوع الجهاز الرئيسي الحالي إلى واجهة المستخدم بشكل غير متزامن. إذا كنت بحاجة إلى مساعدة، يتم تنفيذها في المجلد الذي يحتوي على الحالة النهائية لبرنامج التدريب العملي.