Lớp học lập trình Firebase trên nhiều thiết bị

1. Giới thiệu

Lần cập nhật gần đây nhất: ngày 14 tháng 3 năm 2022

FlutterFire để giao tiếp trên nhiều thiết bị

Khi chứng kiến số lượng lớn các thiết bị tự động hoá nhà, thiết bị đeo và công nghệ sức khoẻ cá nhân xuất hiện trên mạng, giao tiếp giữa các thiết bị trở thành một phần ngày càng quan trọng trong việc xây dựng các ứng dụng di động. Việc thiết lập giao tiếp giữa các thiết bị (chẳng hạn như điều khiển trình duyệt từ một ứng dụng điện thoại hoặc điều khiển nội dung phát trên TV từ điện thoại) thường phức tạp hơn so với việc tạo một ứng dụng di động thông thường .

Cơ sở dữ liệu theo thời gian thực của Firebase cung cấp Presence API (API Trạng thái hoạt động) cho phép người dùng xem trạng thái trực tuyến/ngoại tuyến của thiết bị; bạn sẽ sử dụng API này với Dịch vụ cài đặt Firebase để theo dõi và kết nối tất cả các thiết bị mà cùng một người dùng đã đăng nhập. Bạn sẽ sử dụng Flutter để nhanh chóng tạo các ứng dụng cho nhiều nền tảng, sau đó bạn sẽ tạo một nguyên mẫu đa thiết bị có thể phát nhạc trên một thiết bị và điều khiển nhạc trên một thiết bị khác!

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ tạo một bộ điều khiển từ xa đơn giản cho trình phát nhạc. Ứng dụng này sẽ:

  • Có một trình phát nhạc đơn giản trên Android, iOS và web, được xây dựng bằng Flutter.
  • Cho phép người dùng đăng nhập.
  • Kết nối các thiết bị khi cùng một người dùng đăng nhập trên nhiều thiết bị.
  • Cho phép người dùng điều khiển chế độ phát nhạc trên một thiết bị từ một thiết bị khác.

7f0279938e1d3ab5.gif

Kiến thức bạn sẽ học được

  • Cách tạo và chạy một ứng dụng trình phát nhạc Flutter.
  • Cách cho phép người dùng đăng nhập bằng Firebase Auth.
  • Cách sử dụng Firebase RTDB Presence API và Dịch vụ cài đặt Firebase để kết nối các thiết bị.

Bạn cần

  • Môi trường phát triển Flutter. Làm theo hướng dẫn trong hướng dẫn cài đặt Flutter để thiết lập.
  • Bạn phải dùng Flutter phiên bản tối thiểu là 2.10 trở lên. Nếu bạn có phiên bản thấp hơn, hãy chạy flutter upgrade.
  • Một tài khoản Firebase.

2. Thiết lập

Lấy đoạn mã khởi đầu

Chúng tôi đã tạo một ứng dụng trình phát nhạc bằng Flutter. Mã khởi đầu nằm trong một kho lưu trữ Git. Để bắt đầu, trên dòng lệnh, hãy sao chép kho lưu trữ, chuyển vào thư mục có trạng thái bắt đầu và cài đặt các phần phụ thuộc:

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

cd cross-device-controller/starter_code

flutter pub get

Tạo ứng dụng

Bạn có thể làm việc với IDE mà mình yêu thích để tạo ứng dụng hoặc dùng dòng lệnh.

Trong thư mục ứng dụng, hãy tạo ứng dụng cho web bằng lệnh flutter run -d web-server.. Bạn sẽ thấy lời nhắc sau.

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

Truy cập vào http://localhost:<port> để xem trình phát nhạc.

Nếu đã quen thuộc với trình mô phỏng Android hoặc trình mô phỏng iOS, bạn có thể tạo ứng dụng cho các nền tảng đó và cài đặt ứng dụng bằng lệnh flutter run -d <device_name>.

Ứng dụng web này sẽ hiển thị một trình phát nhạc độc lập cơ bản. Đảm bảo các tính năng của trình phát hoạt động như dự kiến. Đây là một ứng dụng trình phát nhạc đơn giản được thiết kế cho lớp học lập trình này. Bạn chỉ có thể phát bài hát Better Together (Cùng nhau tốt hơn) trên Firebase.

Thiết lập trình mô phỏng Android hoặc trình mô phỏng iOS

Nếu đã có thiết bị Android hoặc thiết bị iOS để phát triển, bạn có thể bỏ qua bước này.

Để tạo một trình mô phỏng Android, hãy tải Android Studio xuống (cũng hỗ trợ việc phát triển Flutter) rồi làm theo hướng dẫn trong bài viết Tạo và quản lý thiết bị ảo.

Để tạo một trình mô phỏng iOS, bạn sẽ cần có môi trường Mac. Tải XCode xuống rồi làm theo hướng dẫn trong phần Tổng quan về trình mô phỏng > Sử dụng trình mô phỏng > Mở và đóng trình mô phỏng.

3. Thiết lập Firebase

Tạo dự án Firebase

  1. Đăng nhập vào bảng điều khiển của Firebase bằng Tài khoản Google của bạn.
  2. Nhấp vào nút này để tạo một dự án mới, rồi nhập tên dự án (ví dụ: Firebase-Cross-Device-Codelab).
  3. Nhấp vào Tiếp tục.
  4. Nếu được nhắc, hãy xem xét và chấp nhận các điều khoản của Firebase, rồi nhấp vào Tiếp tục.
  5. (Không bắt buộc) Bật tính năng hỗ trợ của AI trong bảng điều khiển của Firebase (còn gọi là "Gemini trong Firebase").
  6. Đối với lớp học lập trình này, bạn không cần Google Analytics, vì vậy hãy tắt lựa chọn Google Analytics.
  7. Nhấp vào Tạo dự án, đợi dự án được cấp phép rồi nhấp vào Tiếp tục.

Cài đặt Firebase SDK

Quay lại dòng lệnh, trong thư mục dự án, hãy chạy lệnh sau để cài đặt Firebase:

flutter pub add firebase_core

Trong tệp pubspec.yaml, hãy chỉnh sửa phiên bản cho firebase_core thành ít nhất 1.13.1 hoặc chạy flutter upgrade

Khởi chạy FlutterFire

  1. Nếu chưa cài đặt Giao diện dòng lệnh (CLI) của Firebase, bạn có thể cài đặt bằng cách chạy curl -sL https://firebase.tools | bash.
  2. Đăng nhập bằng cách chạy firebase login và làm theo lời nhắc.
  3. Cài đặt FlutterFire CLI bằng cách chạy dart pub global activate flutterfire_cli.
  4. Định cấu hình FlutterFire CLI bằng cách chạy flutterfire configure.
  5. Tại dấu nhắc, hãy chọn dự án mà bạn vừa tạo cho lớp học lập trình này, chẳng hạn như Firebase-Cross-Device-Codelab.
  6. Chọn iOS, AndroidWeb khi bạn được nhắc chọn chế độ hỗ trợ cấu hình.
  7. Khi được nhắc nhập mã nhận dạng gói Apple, hãy nhập một miền duy nhất hoặc nhập com.example.appname. Bạn có thể nhập mã này cho mục đích của lớp học lập trình này.

Sau khi bạn định cấu hình, một tệp firebase_options.dart sẽ được tạo cho bạn, chứa tất cả các lựa chọn cần thiết để khởi chạy.

Trong trình chỉnh sửa, hãy thêm mã sau vào tệp main.dart để khởi chạy Flutter và 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());
}

Biên dịch ứng dụng bằng lệnh:

flutter run

Bạn chưa thay đổi bất kỳ phần tử nào trên giao diện người dùng, vì vậy, giao diện và hành vi của ứng dụng vẫn chưa thay đổi. Nhưng giờ đây, bạn đã có một ứng dụng Firebase và có thể bắt đầu sử dụng các sản phẩm của Firebase, bao gồm:

  • Xác thực Firebase, cho phép người dùng đăng nhập vào ứng dụng của bạn.
  • Cơ sở dữ liệu theo thời gian thực(RTDB) của Firebase; bạn sẽ sử dụng API trạng thái để theo dõi trạng thái trực tuyến/ngoại tuyến của thiết bị
  • Các quy tắc bảo mật của Firebase sẽ giúp bạn bảo mật cơ sở dữ liệu.
  • Dịch vụ cài đặt Firebase để xác định những thiết bị mà một người dùng đã đăng nhập.

4. Thêm Firebase Auth

Bật tính năng đăng nhập bằng email cho tính năng Xác thực Firebase

Để cho phép người dùng đăng nhập vào ứng dụng web, bạn sẽ phải dùng phương thức đăng nhập Email/Password:

  1. Trong bảng điều khiển của Firebase, hãy mở rộng trình đơn Tạo ở bảng điều khiển bên trái.
  2. Nhấp vào Xác thực, sau đó nhấp vào nút Bắt đầu, rồi nhấp vào thẻ Phương thức đăng nhập.
  3. Nhấp vào Email/Mật khẩu trong danh sách Nhà cung cấp dịch vụ đăng nhập, đặt nút chuyển Bật sang vị trí bật rồi nhấp vào Lưu. 58e3e3e23c2f16a4.png

Định cấu hình Xác thực Firebase trong Flutter

Trên dòng lệnh, hãy chạy các lệnh sau để cài đặt các gói flutter cần thiết:

flutter pub add firebase_auth

flutter pub add provider

Với cấu hình này, giờ đây, bạn có thể tạo quy trình đăng nhập và đăng xuất. Vì trạng thái uỷ quyền không được thay đổi từ màn hình này sang màn hình khác, nên bạn sẽ tạo một lớp application_state.dart để theo dõi các thay đổi về trạng thái ở cấp ứng dụng, chẳng hạn như đăng nhập và đăng xuất. Tìm hiểu thêm về vấn đề này trong tài liệu Quản lý trạng thái Flutter.

Dán nội dung sau vào tệp application_state.dart mới:

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();
  }
}

Để đảm bảo ApplicationState sẽ được khởi tạo khi ứng dụng bắt đầu, bạn sẽ thêm một bước khởi tạo vào 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(),
  ));
}

Một lần nữa, giao diện người dùng của ứng dụng vẫn giữ nguyên, nhưng giờ đây, bạn có thể cho phép người dùng đăng nhập và lưu trạng thái ứng dụng.

Tạo quy trình đăng nhập

Ở bước này, bạn sẽ thực hiện quy trình đăng nhập và đăng xuất. Quy trình sẽ diễn ra như sau:

  1. Người dùng đã đăng xuất sẽ bắt đầu quy trình đăng nhập bằng cách nhấp vào trình đơn theo bối cảnh 71fcc1030a336423.png ở bên phải thanh ứng dụng.
  2. Quy trình đăng nhập sẽ xuất hiện trong một hộp thoại.
  3. Nếu chưa từng đăng nhập trước đây, người dùng sẽ được nhắc tạo tài khoản bằng địa chỉ email hợp lệ và mật khẩu.
  4. Nếu đã đăng nhập trước đó, người dùng sẽ được nhắc nhập mật khẩu.
  5. Sau khi người dùng đăng nhập, khi nhấp vào trình đơn theo bối cảnh, người dùng sẽ thấy lựa chọn Đăng xuất.

c295f6fa2e1d40f3.png

Bạn cần thực hiện 3 bước để thêm quy trình đăng nhập.

Trước hết, hãy tạo một tiện ích AppBarMenuButton. Tiện ích này sẽ kiểm soát cửa sổ bật lên của trình đơn theo bối cảnh, tuỳ thuộc vào loginState của người dùng. Thêm các lệnh nhập

lib/src/widgets.dart

import 'application_state.dart';
import 'package:provider/provider.dart';
import 'authentication.dart';

Thêm mã sau vào 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),
        ),
      ),
    ];
  }
}

Thứ hai, trong cùng một lớp widgets.dart, hãy tạo tiện ích 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,
          ),
        ),
      ]),
    );
  }
}

Thứ ba, hãy tìm tiện ích appBar hiện có trong main.dart. Thêm AppBarMenuButton để hiển thị lựa chọn Đăng nhập hoặc Đăng xuất.

lib/main.dart

import 'src/widgets.dart';
appBar: AppBar(
  title: const Text('Music Box'),
  backgroundColor: Colors.deepPurple.shade400,
  actions: const <Widget>[
    AppBarMenuButton(),
  ],
),

Chạy lệnh flutter run để khởi động lại ứng dụng với những thay đổi này. Bạn sẽ thấy trình đơn theo bối cảnh 71fcc1030a336423.png ở bên phải thanh ứng dụng. Khi nhấp vào nút này, bạn sẽ thấy một hộp thoại đăng nhập.

Sau khi đăng nhập bằng địa chỉ email và mật khẩu hợp lệ, bạn sẽ thấy lựa chọn Đăng xuất trong trình đơn theo bối cảnh.

Trong bảng điều khiển của Firebase, trong mục Xác thực, bạn sẽ thấy địa chỉ email được liệt kê là người dùng mới.

888506c86a28a72c.png

Xin chúc mừng! Giờ đây, người dùng có thể đăng nhập vào ứng dụng!

5. Thêm kết nối cơ sở dữ liệu

Giờ đây, bạn đã sẵn sàng chuyển sang bước đăng ký thiết bị bằng Firebase Presence API.

Trên dòng lệnh, hãy chạy các lệnh sau để thêm các phần phụ thuộc cần thiết:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Tạo cơ sở dữ liệu

Trong bảng điều khiển của Firebase,

  1. Chuyển đến mục Cơ sở dữ liệu theo thời gian thực của bảng điều khiển của Firebase. Nhấp vào Tạo cơ sở dữ liệu.
  2. Nếu được nhắc chọn chế độ bắt đầu cho các quy tắc bảo mật, hãy chọn Chế độ thử nghiệm ngay bây giờ**.** (Chế độ thử nghiệm tạo ra các Quy tắc bảo mật cho phép tất cả các yêu cầu. Bạn sẽ thêm Quy tắc bảo mật sau. Điều quan trọng là bạn không bao giờ được chuyển sang giai đoạn phát hành khi Quy tắc bảo mật vẫn ở Chế độ kiểm thử.)

Hiện tại, cơ sở dữ liệu này đang trống. Tìm databaseURL trong phần Cài đặt dự án, trong thẻ Chung. Di chuyển xuống phần Ứng dụng web.

1b6076f60a36263b.png

Thêm databaseURL vào tệp firebase_options.dart:

lib/firebase_options.dart

 static const FirebaseOptions web = FirebaseOptions(
    apiKey: yourApiKey,
    ...
    databaseURL: 'https://<YOUR_DATABASE_URL>,
    ...
  );

Đăng ký thiết bị bằng RTDB Presence API

Bạn muốn đăng ký thiết bị của người dùng khi họ xuất hiện trên mạng. Để làm việc này, bạn sẽ tận dụng Firebase Installations và Firebase RTDB Presence API để theo dõi danh sách các thiết bị đang trực tuyến của một người dùng. Đoạn mã sau sẽ giúp bạn đạt được mục tiêu này:

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';
  }

Quay lại dòng lệnh, tạo và chạy ứng dụng trên thiết bị hoặc trong trình duyệt bằng flutter run.

Trong ứng dụng, hãy đăng nhập với tư cách là người dùng. Hãy nhớ đăng nhập bằng cùng một người dùng trên các nền tảng khác nhau.

Trong bảng điều khiển của Firebase, bạn sẽ thấy các thiết bị của mình xuất hiện dưới một mã nhận dạng người dùng trong cơ sở dữ liệu.

5bef49cea3564248.png

6. Đồng bộ hoá trạng thái thiết bị

Chọn một thiết bị chính

Để đồng bộ hoá trạng thái giữa các thiết bị, hãy chỉ định một thiết bị làm thiết bị chính hoặc thiết bị điều khiển. Thiết bị chính sẽ quyết định trạng thái trên các thiết bị phụ.

Tạo một phương thức setLeadDevice trong application_state.dart và theo dõi thiết bị này bằng khoá active_device trong 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;
      });
    }
  }

Để thêm chức năng này vào trình đơn theo bối cảnh của thanh ứng dụng, hãy tạo một PopupMenuItem có tên là Controller bằng cách sửa đổi tiện ích SignedInMenuButton. Trình đơn này sẽ cho phép người dùng đặt thiết bị chính.

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();
    }
  }
}

Ghi trạng thái của thiết bị chính vào cơ sở dữ liệu

Sau khi thiết lập một thiết bị chính, bạn có thể đồng bộ hoá các trạng thái của thiết bị chính với RTDB bằng đoạn mã sau. Thêm mã sau vào cuối application_state.dart.Thao tác này sẽ bắt đầu lưu trữ 2 thuộc tính: trạng thái của trình phát (phát hoặc tạm dừng) và vị trí của thanh trượt.

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');
      }
    }
  }

Cuối cùng, bạn cần gọi setActiveDeviceState bất cứ khi nào trạng thái trình phát của bộ điều khiển cập nhật. Thực hiện các thay đổi sau đối với tệp player_widget.dart hiện có:

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;
  }

Đọc trạng thái của thiết bị chính từ cơ sở dữ liệu

Có 2 phần để đọc và sử dụng trạng thái của thiết bị chính. Trước tiên, bạn cần thiết lập một trình nghe cơ sở dữ liệu về trạng thái của trình phát chính trong application_state. Trình nghe này sẽ cho các thiết bị theo dõi biết thời điểm cập nhật màn hình thông qua một lệnh gọi lại. Lưu ý rằng bạn đã xác định một giao diện OnLeadDeviceChangeCallback trong bước này. Chưa triển khai; bạn sẽ triển khai giao diện này trong player_widget.dart ở bước tiếp theo.

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();
      });
    }
  }

Thứ hai, hãy bắt đầu trình nghe cơ sở dữ liệu trong quá trình khởi tạo trình phát trong player_widget.dart. Truyền hàm _updatePlayer để trạng thái người chơi theo dõi có thể được cập nhật bất cứ khi nào giá trị cơ sở dữ liệu thay đổi.

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');
        }
      }
    }
  }

Giờ đây, bạn đã sẵn sàng kiểm thử ứng dụng:

  1. Trên dòng lệnh, hãy chạy ứng dụng trên trình mô phỏng và/hoặc trong trình duyệt bằng cách sử dụng: flutter run -d <device-name>
  2. Mở các ứng dụng trong trình duyệt, trên trình mô phỏng iOS hoặc trình mô phỏng Android. Chuyển đến trình đơn theo bối cảnh, chọn một ứng dụng làm thiết bị chính. Bạn sẽ thấy người chơi trên các thiết bị theo dõi thay đổi khi thiết bị chính cập nhật.
  3. Bây giờ, hãy thay đổi thiết bị chính, phát hoặc tạm dừng nhạc và quan sát các thiết bị phụ cập nhật theo.

Nếu các thiết bị phụ cập nhật đúng cách, tức là bạn đã tạo thành công một bộ điều khiển đa thiết bị. Bạn chỉ còn một bước quan trọng nữa.

7. Cập nhật quy tắc bảo mật

Nếu chúng ta không viết các quy tắc bảo mật tốt hơn, thì người khác có thể ghi trạng thái vào một thiết bị mà họ không sở hữu! Vì vậy, trước khi hoàn tất, hãy cập nhật Quy tắc bảo mật cơ sở dữ liệu theo thời gian thực để đảm bảo rằng chỉ những người dùng đã đăng nhập vào một thiết bị mới có thể đọc hoặc ghi vào thiết bị đó. Trong Bảng điều khiển của Firebase, hãy chuyển đến Cơ sở dữ liệu theo thời gian thực, rồi chuyển đến thẻ Quy tắc. Dán các quy tắc sau đây để chỉ cho phép người dùng đã đăng nhập đọc và ghi trạng thái thiết bị của chính họ:

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

8. Xin chúc mừng!

bcd986f7106d892b.gif

Xin chúc mừng! Bạn đã tạo thành công một bộ điều khiển từ xa đa thiết bị bằng Flutter!

Tín dụng

Better Together, một bài hát về Firebase

  • Nhạc của Ryan Vernon
  • Lời bài hát và ảnh bìa album của Marissa Christy
  • Giọng nói của JP Gomez

9. Điểm thưởng

Để tăng thêm độ khó, hãy cân nhắc sử dụng Flutter FutureBuilder để thêm loại thiết bị chính hiện tại vào giao diện người dùng một cách không đồng bộ. Nếu bạn cần trợ giúp, hãy triển khai trong thư mục chứa trạng thái hoàn chỉnh của lớp học lập trình.

Tài liệu tham khảo và các bước tiếp theo