Firebase Cross Device Codelab

1. مقدمه

آخرین به روز رسانی: 2022-03-14

FlutterFire برای ارتباط متقابل دستگاه

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

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

چیزی که خواهی ساخت

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

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

7f0279938e1d3ab5.gif

چیزی که یاد خواهید گرفت

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

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

  • یک محیط توسعه فلاتر. دستورالعمل های راهنمای نصب Flutter را برای تنظیم آن دنبال کنید.
  • حداقل نسخه فلاتر 2.10 یا بالاتر مورد نیاز است. اگر نسخه پایین تری دارید، flutter upgrade.
  • یک حساب Firebase

2. راه اندازی

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

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

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

cd cross-device-controller/starter_code

flutter pub get

برنامه را بسازید

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

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

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

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

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

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

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

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

برای ایجاد یک شبیه‌ساز اندروید، Android Studio را دانلود کنید که از توسعه Flutter نیز پشتیبانی می‌کند و دستورالعمل‌های موجود در Create and management devices مجازی را دنبال کنید.

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

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 CLI را نصب نکرده اید، می توانید آن را با اجرای curl -sL https://firebase.tools | bash .
  2. با اجرای firebase login و دنبال کردن دستورات وارد سیستم شوید.
  3. FlutterFire CLI را با اجرای dart pub global activate flutterfire_cli نصب کنید.
  4. با اجرای flutterfire configure FlutterFire CLI را پیکربندی کنید.
  5. در اعلان، پروژه ای را که به تازگی برای این Codelab ایجاد کرده اید، انتخاب کنید، چیزی مانند Firebase-Cross-Device-Codelab .
  6. وقتی از شما خواسته شد که پشتیبانی پیکربندی را انتخاب کنید، iOS ، Android و Web را انتخاب کنید.
  7. وقتی از اپل باندل ID خواسته شد، یک دامنه منحصر به فرد را تایپ کنید یا com.example.appname وارد کنید، که برای اهداف این Codelab مناسب است.

پس از پیکربندی، یک فایل 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) ; شما از API حضور برای ردیابی وضعیت آنلاین/آفلاین دستگاه استفاده خواهید کرد
  • قوانین امنیتی Firebase به شما این امکان را می دهد که پایگاه داده را ایمن کنید.
  • Firebase Installations Service برای شناسایی دستگاه‌هایی که یک کاربر به سیستم وارد شده است.

4. Firebase Auth را اضافه کنید

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

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

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

احراز هویت Firebase را در 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. پس از ورود کاربر، با کلیک بر روی منوی زمینه، گزینه Sign out نمایش داده می شود.

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 را اضافه کنید تا گزینه Sign in یا Sign out نمایش داده شود.

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

888506c86a28a72c.png

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

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

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

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

flutter pub add firebase_app_installations

flutter pub add firebase_database

یک پایگاه داده ایجاد کنید

در کنسول Firebase،

  1. به بخش Realtime Database کنسول 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 Installations و Firebase RTDB Presence API برای پیگیری فهرستی از دستگاه های آنلاین از یک کاربر استفاده خواهید کرد. کد زیر به انجام این هدف کمک می کند:

lib/src/application_state.dart

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart'; 

class ApplicationState extends ChangeNotifier {

  String? _deviceId;
  String? _uid;

  Future<void> init() async {
    ...
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _uid = user.uid;
        _addUserDevice();
      }
      ...
    });
  }

  Future<void> _addUserDevice() async {
    _uid = FirebaseAuth.instance.currentUser?.uid;

    String deviceType = _getDevicePlatform();
    // Create two objects which we will write to the
    // Realtime database when this device is offline or online
    var isOfflineForDatabase = {
      'type': deviceType,
      'state': 'offline',
      'last_changed': ServerValue.timestamp,
    };
    var isOnlineForDatabase = {
      'type': deviceType,
      'state': 'online',
      'last_changed': ServerValue.timestamp,
    };

    var devicesRef =
        FirebaseDatabase.instance.ref().child('/users/$_uid/devices');

    FirebaseInstallations.instance
        .getId()
        .then((id) => _deviceId = id)
        .then((_) {
      // Use the semi-persistent Firebase Installation Id to key devices
      var deviceStatusRef = devicesRef.child('$_deviceId');

      // RTDB Presence API
      FirebaseDatabase.instance
          .ref()
          .child('.info/connected')
          .onValue
          .listen((data) {
        if (data.snapshot.value == false) {
          return;
        }

        deviceStatusRef.onDisconnect().set(isOfflineForDatabase).then((_) {
          deviceStatusRef.set(isOnlineForDatabase);
        });
      });
    });
  }

  String _getDevicePlatform() {
    if (kIsWeb) {
      return 'Web';
    } else if (Platform.isIOS) {
      return 'iOS';
    } else if (Platform.isAndroid) {
      return 'Android';
    }
    return 'Unknown';
  }

به خط فرمان برگردید، برنامه را در دستگاه خود یا در یک مرورگر با flutter run.

در برنامه خود، به عنوان کاربر وارد سیستم شوید. به یاد داشته باشید که به عنوان یک کاربر در سیستم عامل های مختلف وارد سیستم شوید.

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

5bef49cea3564248.png

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

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

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

یک متد setLeadDevice در application_state.dart ایجاد کنید و این دستگاه را با کلید active_device در RTDB ردیابی کنید:

lib/src/application_state.dart

  bool _isLeadDevice = false;
  String? leadDeviceType;

  Future<void> setLeadDevice() async {
    if (_uid != null && _deviceId != null) {
      var playerRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      await playerRef
          .update({'id': _deviceId, 'type': _getDevicePlatform()}).then((_) {
        _isLeadDevice = true;
      });
    }
  }

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

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

7. قوانین امنیتی را به روز کنید

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

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

8. تبریک می گویم!

bcd986f7106d892b.gif

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

اعتبارات

Better Together, a Firebase Song

  • موسیقی از رایان ورنون
  • شعر و جلد آلبوم از ماریسا کریستی
  • صدای جی پی گومز

9. پاداش

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

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