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 03 năm 2022

FlutterFire giúp giao tiếp trên nhiều thiết bị

Khi chúng ta chứng kiến một số lượng lớn thiết bị tự động hoá nhà, thiết bị đeo được và thiết bị công nghệ sức khoẻ cá nhân sắp xuất hiện trên mạng, việc giao tiếp giữa các thiết bị ngày càng trở thành một phần quan trọng trong việc xây dựng ứng dụng di động. Theo truyền thống, việc thiết lập hoạt động giao tiếp trên nhiều thiết bị như điều khiển trình duyệt từ ứng dụng dành cho điện thoại hoặc điều khiển nội dung phát trên TV bằng điện thoại sẽ phức tạp hơn so với việc xây dựng 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 API hiện diện , giúp người dùng xem trạng thái của thiết bị khi có kết nối mạng/ngoại tuyến; bạn sẽ sử dụng nó với Dịch vụ cài đặt Firebase để theo dõi và kết nối tất cả thiết bị mà cùng một người dùng đã đăng nhập. Bạn sẽ dùng Flutter để nhanh chóng tạo ứng dụng cho nhiều nền tảng, sau đó sẽ xây dựng một nguyên mẫu trên nhiều thiết bị để 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 cho trình phát nhạc đơn giản. Ứng dụng này sẽ:

  • Sở hữu 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 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 ứng dụng phát nhạc Flutter.
  • Cách cho phép người dùng đăng nhập bằng tính năng Xác thực Firebase.
  • Cách sử dụng API Hiện diện RTDB của Firebase và Dịch vụ cài đặt Firebase để kết nối thiết bị.

Bạn cần có

  • 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 cần có phiên bản Flutter 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.
  • Tài khoản Firebase.

2. Thiết lập

Lấy mã khởi đầu

Chúng tôi đã tạo một ứng dụng trình phát nhạc trong Flutter. Mã khởi đầu nằm trong kho lưu trữ Git. Để bắt đầu, trên dòng lệnh, hãy sao chép kho lưu trữ, di chuyển vào thư mục có trạng thái bắt đầu rồi 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

Xây dựng ứ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 sử 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 đây.

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

Truy cập 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 những nền tảng đó rồi cài đặt bằng lệnh flutter run -d <device_name>.

Ứng dụng web phải hiển thị một trình phát nhạc độc lập cơ bản. Hãy đả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. Tính năng này chỉ có thể phát một bài hát trong Firebase, Better Together.

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 iOS để phát triển, bạn có thể bỏ qua bước này.

Để tạo trình mô phỏng Android, hãy tải Android Studio (cũng hỗ trợ phát triển Flutter) xuống, 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 trình mô phỏng iOS, bạn cần có môi trường Mac. Tải XCode xuống rồi làm theo hướng dẫn trong mục 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

Mở trình duyệt để http://console.firebase.google.com/.

  1. Đăng nhập vào Firebase.
  2. Trong bảng điều khiển của Firebase, hãy nhấp vào Thêm dự án (hoặc Tạo dự án) rồi đặt tên cho dự án Firebase của bạn là Firebase-Cross-Device-Codelab.
  3. Nhấp vào các lựa chọn tạo dự án. Chấp nhận các điều khoản của Firebase nếu được nhắc. Bỏ qua bước thiết lập Google Analytics vì bạn sẽ không sử dụng Analytics cho ứng dụng này.

Bạn không cần tải các tệp được đề cập xuống hoặc thay đổi các tệp build.gradle. Bạn sẽ định cấu hình chúng khi khởi chạy FlutterFire.

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 1.13.1 trở lên, hoặc chạy flutter upgrade

Chạy FlutterFire

  1. Nếu chưa cài đặt CLI của Firebase, bạn có thể cài đặt giao diện này 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 lời nhắc, hãy chọn dự án 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 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ể sử dụng 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, trong đó chứa tất cả tuỳ 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 thành phần 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 chưa thay đổi. Nhưng bây giờ, 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 của Firebase(RTDB); bạn sẽ dùng API hiện diện để theo dõi trạng thái trực tuyến/ngoại tuyến của thiết bị
  • Quy tắc bảo mật của Firebase sẽ cho phép bạn bảo mật cơ sở dữ liệu.
  • Dịch vụ cài đặt Firebase để xác định thiết bị mà một người dùng đã đăng nhập.

4. Thêm phương thức xác thực Firebase

Bật tính năng đăng nhập 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 cần sử dụng phương thức đăng nhập Email/Mật khẩu:

  1. Trong bảng điều khiển của Firebase, hãy mở rộng trình đơn Build (Tạo) trong 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, sau đó 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, chuyển nút chuyển Bật sang vị trí bật rồi nhấp vào Lưu. 58e3e3e23c2f16a4.pngS

Định cấu hình tính năng 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 xác thực không thay đổi giữa các màn hình, 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 về Quản lý trạng thái Flutter.

Dán các dòng 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 chạy khi ứng dụng khởi động, bạn sẽ thêm một bước khởi chạy 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(),
  ));
}

Xin nhắc lại, giao diện người dùng của ứng dụng lẽ ra vẫn được 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. Luồng này sẽ có dạng 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.pngSở bên phải thanh ứng dụng.
  2. Quy trình đăng nhập sẽ hiển thị trong hộp thoại.
  3. Nếu trước đây người dùng chưa từng đăng nhập, họ sẽ được nhắc tạo một tài khoản bằng địa chỉ email và mật khẩu hợp lệ.
  4. Nếu người dùng đã đăng nhập trước đó, họ sẽ được nhắc nhập mật khẩu của mình.
  5. Sau khi người dùng đăng nhập, thao tác nhấp vào trình đơn theo bối cảnh sẽ hiển thị tuỳ chọn Đăng xuất.

c295f6fa2e1d40f3.pngS.

Quy trình thêm quy trình đăng nhập cần 3 bước.

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),
        ),
      ),
    ];
  }
}

Tiếp theo, 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, tìm tiện ích appBar hiện có trong phần main.dart. Thêm AppBarMenuButton để hiển thị tuỳ chọn Sign in (Đăng nhập) hoặc Sign out (Đă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 các thay đổi này. Bạn có thể thấy trình đơn theo bối cảnh 71fcc1030a336423.pngS ở bên phải thanh ứng dụng. Khi nhấp vào đường liên kết này, bạn sẽ được chuyển đến 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 có thể 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.pngS

Xin chúc mừng! Người dùng hiện có thể đăng nhập vào ứng dụng!

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

Bây giờ, bạn đã sẵn sàng chuyển sang phương thức đăng ký thiết bị bằng API Sự hiện diện của Firebase.

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 trong 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 vượt qua tất cả các yêu cầu. Bạn sẽ thêm Quy tắc bảo mật vào lúc khác. Điều quan trọng là không bao giờ chuyển sang giai đoạn phát hành chính thức khi Quy tắc bảo mật vẫn đang ở Chế độ thử nghiệm.)

Cơ sở dữ liệu hiện đang trống. Xác định vị trí databaseURL của bạn trong phần Project settings (Cài đặt dự án) ở thẻ General (Chung). Di chuyển xuống phần Ứng dụng web.

1b6076f60a36263b.png.

Thêm databaseURL của bạn 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 API hiện diện RTDB

Bạn muốn đăng ký thiết bị của người dùng khi thiết bị của bạn xuất hiện trên mạng. Để thực hiện việc này, bạn sẽ tận dụng tính năng Cài đặt Firebase và API hiện diện của Firebase RTDB để theo dõi danh sách thiết bị trực tuyến của một người dùng. Mã sau đây sẽ giúp đạ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ị của bạn hoặc trong trình duyệt bằng flutter run.

Trong ứng dụng của bạn, 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 của bạn.

5bef49cea3564248.pngS

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

Chọn một thiết bị khách hàng tiềm năng

Để đồ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 bộ điều khiển. Thiết bị khách hàng tiềm năng sẽ chỉ ra các trạng thái trên các thiết bị khách hàng tiềm năng.

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ị khách hàng tiềm năng.

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ị khách hàng tiềm năng vào cơ sở dữ liệu

Sau khi thiết lập thiết bị khách hàng tiềm năng, bạn có thể đồng bộ hoá trạng thái của thiết bị khách hàng tiềm năng với RTDB bằng 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ữ hai thuộc tính: trạng thái trình phát (phát hoặc tạm dừng) và vị trí 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 của người chơi trên tay đ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 tại:

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ị khách hàng tiềm năng từ cơ sở dữ liệu

Có hai phần để đọc và sử dụng trạng thái của thiết bị khách hàng tiềm năng. Trước tiên, bạn muốn thiết lập trình nghe cơ sở dữ liệu của trạng thái người chơi khách hàng tiềm năng 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. Xin lưu ý rằng bạn đã xác định giao diện OnLeadDeviceChangeCallback trong bước này. Tính năng này chưa được 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 khởi động trình nghe cơ sở dữ liệu trong quá trình khởi chạy trình phát trong player_widget.dart. Truyền hàm _updatePlayer để có thể cập nhật trạng thái của người chơi theo dõi mỗi khi 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ờ 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 có: flutter run -d <device-name>
  2. Mở ứng dụng trong một 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ẽ có thể thấy các thiết bị theo dõi người chơi thay đổi khi thiết bị dẫn đầu 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ị của người theo dõi đang cập nhật tương ứng.

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

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

Trừ phi chúng ta viết các quy tắc bảo mật tốt hơn, ai đó có thể ghi trạng thái cho 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 người dùng đã đăng nhập vào thiết bị là người dùng duy nhất 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, sau đó 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 riêng 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 cho nhiều thiết bị bằng Flutter!

Tín dụng

Bài hát Better Together, một bài hát trong Firebase

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

9. Điểm thưởng

Một thách thức khác, hãy cân nhắc sử dụng Flutter FutureBuilder để thêm loại thiết bị khách hàng tiềm năng 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 được hỗ trợ, nội dung hỗ trợ sẽ được triển khai trong thư mục chứa trạng thái hoàn tất 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