Firebase Cross Device Codelab

۱. مقدمه

آخرین به‌روزرسانی: ۱۴-۰۳-۲۰۲۲

FlutterFire برای ارتباط بین دستگاهی

همانطور که شاهد آنلاین شدن تعداد زیادی از دستگاه‌های اتوماسیون خانگی، پوشیدنی‌ها و فناوری‌های سلامت شخصی هستیم، ارتباط بین دستگاهی به بخش مهمی از ساخت برنامه‌های کاربردی موبایل تبدیل می‌شود. راه‌اندازی ارتباط بین دستگاهی مانند کنترل مرورگر از طریق برنامه تلفن همراه یا کنترل آنچه در تلویزیون شما از طریق تلفن همراه پخش می‌شود، به طور سنتی پیچیده‌تر از ساخت یک برنامه کاربردی موبایل معمولی است.

پایگاه داده بلادرنگ فایربیس، رابط برنامه‌نویسی کاربردی (API) Presence را ارائه می‌دهد که به کاربران امکان می‌دهد وضعیت آنلاین/آفلاین بودن دستگاه خود را مشاهده کنند. شما از آن به همراه سرویس نصب فایربیس برای ردیابی و اتصال تمام دستگاه‌هایی که یک کاربر در آنها وارد سیستم شده است، استفاده خواهید کرد. شما از فلاتر برای ایجاد سریع برنامه‌های کاربردی برای چندین پلتفرم استفاده خواهید کرد و سپس یک نمونه اولیه چند دستگاهی خواهید ساخت که موسیقی را در یک دستگاه پخش می‌کند و موسیقی را در دستگاه دیگر کنترل می‌کند!

آنچه خواهید ساخت

در این آزمایشگاه کد، شما یک کنترل از راه دور پخش کننده موسیقی ساده خواهید ساخت. برنامه شما:

  • یک پخش‌کننده موسیقی ساده برای اندروید، iOS و وب داشته باشید که با Flutter ساخته شده است.
  • به کاربران اجازه ورود بدهید.
  • اتصال دستگاه‌ها زمانی که یک کاربر در چندین دستگاه وارد سیستم شده است.
  • به کاربران اجازه دهید پخش موسیقی از یک دستگاه را از دستگاه دیگر کنترل کنند.

7f0279938e1d3ab5.gif

آنچه یاد خواهید گرفت

  • نحوه ساخت و اجرای یک برنامه پخش موسیقی Flutter.
  • چگونه به کاربران اجازه دهیم با Firebase Auth وارد سیستم شوند.
  • نحوه استفاده از API Firebase RTDB Presence و سرویس نصب Firebase برای اتصال دستگاه‌ها.

آنچه نیاز دارید

  • یک محیط توسعه Flutter. برای راه‌اندازی آن، دستورالعمل‌های موجود در راهنمای نصب Flutter را دنبال کنید.
  • حداقل نسخه فلاتر ۲.۱۰ یا بالاتر مورد نیاز است. اگر نسخه پایین‌تری دارید، flutter upgrade.
  • یک حساب کاربری فایربیس.

۲. راه‌اندازی

کد شروع را دریافت کنید

ما یک برنامه پخش کننده موسیقی در Flutter ایجاد کرده‌ایم. کد شروع در یک مخزن Git قرار دارد. برای شروع، در خط فرمان، مخزن را کپی کنید، به پوشه‌ای که حالت شروع را دارد بروید و وابستگی‌ها را نصب کنید:

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

cd cross-device-controller/starter_code

flutter pub get

ساخت اپلیکیشن

شما می‌توانید با IDE مورد علاقه خود برای ساخت برنامه کار کنید، یا از خط فرمان استفاده کنید.

در دایرکتوری app خود، برنامه را برای وب با دستور flutter run -d web-server. باید بتوانید اعلان زیر را مشاهده کنید.

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

برای مشاهده پخش کننده موسیقی، به http://localhost:<port> مراجعه کنید.

اگر با شبیه‌ساز اندروید یا شبیه‌ساز iOS آشنا هستید، می‌توانید برنامه را برای آن پلتفرم‌ها بسازید و با دستور flutter run -d <device_name> آن را نصب کنید.

برنامه وب باید یک پخش‌کننده موسیقی مستقل و ساده را نشان دهد. مطمئن شوید که ویژگی‌های پخش‌کننده طبق انتظار کار می‌کنند. این یک برنامه پخش‌کننده موسیقی ساده است که برای این codelab طراحی شده است. این برنامه فقط می‌تواند یک آهنگ Firebase به نام Better Together را پخش کند.

یک شبیه‌ساز اندروید یا یک شبیه‌ساز iOS راه‌اندازی کنید

اگر از قبل یک دستگاه اندروید یا iOS برای توسعه دارید، می‌توانید از این مرحله صرف نظر کنید.

برای ایجاد یک شبیه‌ساز اندروید، اندروید استودیو را که از توسعه فلاتر نیز پشتیبانی می‌کند، دانلود کنید و دستورالعمل‌های موجود در بخش «ایجاد و مدیریت دستگاه‌های مجازی» را دنبال کنید.

برای ایجاد یک شبیه‌ساز iOS، به یک محیط مک نیاز دارید. XCode را دانلود کنید و دستورالعمل‌های موجود در Simulator Overview > Use Simulator > Open and close a simulator را دنبال کنید.

۳. فایربیس را راه‌اندازی کنید

ایجاد یک پروژه فایربیس

  1. با استفاده از حساب گوگل خود وارد کنسول فایربیس شوید.
  2. برای ایجاد یک پروژه جدید، روی دکمه کلیک کنید و سپس نام پروژه را وارد کنید (برای مثال، Firebase-Cross-Device-Codelab ).
  3. روی ادامه کلیک کنید.
  4. در صورت درخواست، شرایط Firebase را مرور و قبول کنید و سپس روی ادامه کلیک کنید.
  5. (اختیاری) دستیار هوش مصنوعی را در کنسول Firebase (با نام "Gemini در Firebase") فعال کنید.
  6. برای این codelab، به گوگل آنالیتیکس نیاز ندارید ، بنابراین گزینه گوگل آنالیتیکس را غیرفعال کنید .
  7. روی ایجاد پروژه کلیک کنید، منتظر بمانید تا پروژه شما آماده شود و سپس روی ادامه کلیک کنید.

نصب فایربیس SDK

دوباره در خط فرمان، در دایرکتوری پروژه، دستور زیر را برای نصب Firebase اجرا کنید:

flutter pub add firebase_core

در فایل pubspec.yaml ، نسخه firebase_core را ویرایش کنید تا حداقل ۱.۱۳.۱ باشد، یا flutter upgrade اجرا کنید.

مقداردهی اولیه FlutterFire

  1. اگر رابط خط فرمان فایربیس را نصب ندارید، می‌توانید با اجرای curl -sL https://firebase.tools | bash آن را نصب کنید.
  2. با اجرای دستور firebase login و دنبال کردن دستورالعمل‌ها، وارد سیستم شوید.
  3. با اجرای دستور dart pub global activate flutterfire_cli رابط خط فرمان FlutterFire را نصب کنید.
  4. با اجرای flutterfire configure رابط خط فرمان FlutterFire را پیکربندی کنید.
  5. در اعلان، پروژه‌ای را که برای این codelab ایجاد کرده‌اید، چیزی شبیه به Firebase-Cross-Device-Codelab ، انتخاب کنید.
  6. وقتی از شما خواسته شد پشتیبانی پیکربندی را انتخاب کنید ، iOS ، اندروید و وب را انتخاب کنید.
  7. وقتی از شما شناسه بسته اپل (Apple bundle ID) خواسته شد، یک دامنه منحصر به فرد تایپ کنید، یا 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 (RTDB) ؛ شما از API حضور برای ردیابی وضعیت آنلاین/آفلاین دستگاه استفاده خواهید کرد.
  • قوانین امنیتی فایربیس به شما امکان می‌دهد پایگاه داده را ایمن کنید.
  • سرویس Firebase Installations برای شناسایی دستگاه‌هایی که یک کاربر واحد در آنها وارد سیستم شده است.

۴. اضافه کردن احراز هویت فایربیس

فعال کردن ورود از طریق ایمیل برای احراز هویت فایربیس

برای اینکه به کاربران اجازه دهید وارد برنامه وب شوند، از متد ورود با ایمیل/رمز عبور استفاده خواهید کرد:

  1. در کنسول Firebase، منوی Build را در پنل سمت چپ باز کنید.
  2. روی تأیید اعتبار کلیک کنید و سپس روی دکمه شروع به کار کلیک کنید، و سپس به برگه روش ورود بروید .
  3. در فهرست ارائه‌دهندگان ورود ، روی ایمیل/رمز عبور کلیک کنید، کلید فعال‌سازی را روی حالت روشن قرار دهید و سپس روی ذخیره کلیک کنید. 58e3e3e23c2f16a4.png

پیکربندی احراز هویت فایربیس در فلاتر

در خط فرمان، دستورات زیر را برای نصب بسته‌های لازم فلاتر اجرا کنید:

flutter pub add firebase_auth

flutter pub add provider

با این پیکربندی، اکنون می‌توانید جریان ورود و خروج را ایجاد کنید. از آنجایی که وضعیت احراز هویت نباید از صفحه‌ای به صفحه دیگر تغییر کند، یک کلاس application_state.dart ایجاد خواهید کرد تا تغییرات وضعیت سطح برنامه، مانند ورود و خروج، را پیگیری کند. برای اطلاعات بیشتر در مورد این موضوع، به مستندات مدیریت وضعیت فلاتر مراجعه کنید.

موارد زیر را در فایل جدید 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، در قسمت Authentication ، باید بتوانید آدرس ایمیلی که به عنوان کاربر جدید فهرست شده است را ببینید.

۸۸۸۵۰۶c۸۶a۲۸a۷۲c.png

تبریک! کاربران اکنون می‌توانند وارد برنامه شوند!

۵. اتصال به پایگاه داده را اضافه کنید

اکنون آماده‌اید تا با استفاده از API مربوط به Firebase Presence، به سراغ ثبت دستگاه بروید.

در خط فرمان، دستورات زیر را برای اضافه کردن وابستگی‌های لازم اجرا کنید:

flutter pub add firebase_app_installations

flutter pub add firebase_database

ایجاد پایگاه داده

در کنسول فایربیس،

  1. به بخش پایگاه داده‌ی بلادرنگ (Realtime Database) در کنسول فایربیس بروید. روی ایجاد پایگاه داده (Create Database) کلیک کنید.
  2. اگر از شما خواسته شد که یک حالت شروع برای قوانین امنیتی خود انتخاب کنید، فعلاً حالت آزمایشی را انتخاب کنید**.** (حالت آزمایشی، قوانین امنیتی ایجاد می‌کند که به همه درخواست‌ها اجازه عبور می‌دهد. قوانین امنیتی را بعداً اضافه خواهید کرد. مهم است که هرگز با قوانین امنیتی خود که هنوز در حالت آزمایشی هستند، به محیط عملیاتی نروید.)

فعلاً پایگاه داده خالی است. databaseURL خود را در تنظیمات پروژه ، در زیر برگه عمومی ، پیدا کنید. به پایین اسکرول کنید تا به بخش برنامه‌های وب برسید.

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

۶. همگام‌سازی وضعیت دستگاه

یک دستگاه پیشرو انتخاب کنید

برای همگام‌سازی وضعیت بین دستگاه‌ها، یک دستگاه را به عنوان رهبر یا کنترل‌کننده تعیین کنید. دستگاه رهبر، وضعیت دستگاه‌های پیرو را تعیین می‌کند.

یک متد 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;
      });
    }
  }

برای افزودن این قابلیت به منوی زمینه نوار برنامه، با تغییر ویجت SignedInMenuButton ، یک PopupMenuItem به نام Controller ایجاد کنید. این منو به کاربران امکان می‌دهد دستگاه اصلی را تنظیم کنند.

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 یا یک شبیه‌ساز اندروید باز کنید. به منوی زمینه بروید، یک برنامه را به عنوان دستگاه رهبر انتخاب کنید. باید بتوانید ببینید که پخش‌کننده‌های دستگاه‌های پیرو با به‌روزرسانی دستگاه رهبر تغییر می‌کنند.
  3. حالا دستگاه رهبر را تغییر دهید، موسیقی را پخش یا متوقف کنید و به‌روزرسانی دستگاه‌های پیرو را بر اساس آن مشاهده کنید.

اگر دستگاه‌های پیرو به درستی به‌روزرسانی شوند، شما در ساخت یک کنترلر چند دستگاهی موفق شده‌اید. فقط یک مرحله مهم باقی مانده است.

۷. به‌روزرسانی قوانین امنیتی

مگر اینکه قوانین امنیتی بهتری بنویسیم، کسی می‌تواند وضعیتی را روی دستگاهی که مالک آن نیست بنویسد! بنابراین قبل از اتمام، قوانین امنیتی پایگاه داده Realtime را به‌روزرسانی کنید تا مطمئن شوید تنها کاربرانی که می‌توانند یک دستگاه را بخوانند یا بنویسند، کاربری هستند که به آن دستگاه وارد شده است. در کنسول Firebase، به پایگاه داده Realtime و سپس به تب Rules بروید. قوانین زیر را که فقط به کاربران وارد شده اجازه خواندن و نوشتن وضعیت دستگاه خود را می‌دهد، جایگذاری کنید:

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

۸. تبریک می‌گویم!

bcd986f7106d892b.gif

تبریک می‌گویم، شما با موفقیت یک کنترل از راه دور بین دستگاه‌های مختلف با استفاده از فلاتر ساختید!

اعتبارات

بهتر است با هم، یک آهنگ فایربیس

  • موسیقی ساخته شده توسط: رایان ورنون
  • متن ترانه‌ها و جلد آلبوم از ماریسا کریستی
  • صداپیشه: جی پی گومز

۹. پاداش

به عنوان یک چالش اضافه، استفاده از Flutter FutureBuilder را برای اضافه کردن نوع دستگاه فعلی lead به رابط کاربری به صورت غیرهمزمان در نظر بگیرید. اگر به کمک نیاز دارید، این کمک در پوشه‌ای که حاوی وضعیت نهایی codelab است، پیاده‌سازی شده است.

اسناد مرجع و مراحل بعدی