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

1. Giới thiệu

Cập nhật lần cuối: 2022-03-14

FlutterFire để liên lạc giữa các thiết bị

Khi chúng ta chứng kiến ​​một số lượng lớn các thiết bị công nghệ sức khỏe cá nhân, thiết bị đeo và tự động hóa gia đình xuất hiện trực tuyến, 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 các ứng dụng di động. Thiết lập liên lạc giữa các thiết bị, chẳng hạn như điều khiển trình duyệt từ ứng dụng điện thoại hoặc kiểm soát nội dung phát trên TV từ điện thoại, theo truyền thống 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 thời gian thực của Firebase cung cấp API hiện diện 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 nó 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 ứng dụng cho nhiều nền tảng và sau đó bạn sẽ xây dựng một nguyên mẫu thiết bị chéo để phát nhạc trên một thiết bị và điều khiển nhạc trên thiết bị khác!

Những gì bạn sẽ xây dựng

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

  • Có 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 việc phát lại nhạc trên thiết bị này từ thiết bị khác.

7f0279938e1d3ab5.gif

Bạn sẽ học được gì

  • Cách xây dựng và chạy ứ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 API hiện diện Firebase RTDB và Dịch vụ cài đặt Firebase để kết nối các thiết bị.

Những gì bạn cần

  • Một 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.
  • Cần có phiên bản Flutter tối thiểu từ 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. Bắt đầu 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 động nằm trong kho lưu trữ Git. Để bắt đầu, trên dòng lệnh, sao chép repo, di 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

Xây dựng ứng dụng

Bạn có thể làm việc với IDE yêu thích của mình để xây dựng ứng dụng hoặc sử dụng dòng lệnh.

Trong thư mục ứng dụng của bạn, hãy xây dựng ứng dụng cho web bằng lệnh flutter run -d web-server. Bạn sẽ có thể nhìn 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 với trình mô phỏng Android hoặc trình mô phỏng iOS, bạn có thể xây dựng ứng dụng cho các nền tảng đó và cài đặt nó bằng lệnh flutter run -d <device_name> .

Ứng dụng web 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 đang hoạt động như dự định. Đâ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. Nó chỉ có thể phát một bài hát của Firebase, Better Together .

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

Nếu bạn đã 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 trình giả lập Android, hãy tải xuống Android Studio cũng hỗ trợ phát triển Flutter và làm theo hướng dẫn trong Tạo và quản lý thiết bị ảo .

Để tạo trình giả lập iOS, bạn sẽ cần môi trường Mac. Tải xuống XCode và làm theo hướng dẫn trong 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 tới http://console.firebase.google.com/ .

  1. Đăng nhập vào Firebase .
  2. Trong bảng điều khiển Firebase, hãy nhấp vào Thêm dự án (hoặc Tạo dự án ) và đặt tên cho dự án Firebase của bạn là Firebase-Cross-Device-Codelab .
  3. Nhấp qua các tùy 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 việ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 xuống các tệp được đề cập hoặc thay đổi tệp build.gradle. Bạn sẽ định cấu hình chúng khi khởi tạo FlutterFire.

Cài đặt SDK Firebase

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

flutter pub add firebase_core

Trong file pubspec.yaml sửa phiên bản cho firebase_core tối thiểu là 1.13.1 hoặc chạy flutter upgrade

Khởi tạo FlutterFire

  1. Nếu chưa cài đặt Firebase CLI, bạn có thể cài đặt nó 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. Khi được 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 ID gói Apple , hãy nhập một miền duy nhất hoặc nhập com.example.appname , điều này phù hợp với mục đích của lớp học lập trình này.

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

Trong trình chỉnh sửa của bạn, 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ỳ thành phần giao diện người dùng nào nên giao diện và hoạt động của ứng dụng không thay đổi. Nhưng bây giờ bạn đã có ứng dụng Firebase và có thể bắt đầu sử dụng các sản phẩm 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 thời gian thực Firebase (RTDB) ; bạn sẽ 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 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 các thiết bị mà một người dùng đã đăng nhập.

4. Thêm xác thực Firebase

Cho phép đăng nhập email để xác thực Firebase

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

  1. Trong bảng điều khiển Firebase, hãy mở rộng menu Build ở bảng điều khiển bên trái.
  2. Nhấp vào Xác thực rồi nhấp vào nút Bắt đầu , sau đó nhấp vào tab 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 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, chạy các lệnh sau để cài đặt các gói rung 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 luồng đăng nhập và đăng xuất. Vì trạng thái xác thực không nên thay đổi từ màn hình này sang màn hình khác nên bạn sẽ tạo lớp application_state.dart để theo dõi các thay đổi 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ề điều này trong tài liệu quản lý trạng thái Flutter .

Dán phần 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 khởi động, bạn sẽ thêm 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 ứng dụng lẽ ra 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 luồng đăng nhập

Ở bước này, bạn sẽ thực hiện quy trình đăng nhập và đăng xuất. Đây là dòng chảy sẽ trông như thế nào:

  1. Người dùng đã đăng xuất sẽ bắt đầu luồng đăng nhập bằng cách nhấp vào menu ngữ cảnh 71fcc1030a336423.png ở phía bên phải của thanh ứng dụng.
  2. Luồng đăng nhập sẽ được hiển thị trong hộp thoại.
  3. Nếu người dùng chưa từng đăng nhập trước đó, họ sẽ được nhắc tạo 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. Khi người dùng đã đăng nhập, nhấp vào menu ngữ cảnh sẽ hiển thị tùy chọn Đăng xuất .

c295f6fa2e1d40f3.png

Việc thêm luồng đăng nhập yêu cầu ba bước.

Trước hết, hãy tạo tiện ích AppBarMenuButton . Tiện ích này sẽ kiểm soát cửa sổ bật lên của menu ngữ cảnh tùy thuộc vào loginState của người dùng. Thêm hàng nhập khẩu

lib/src/widgets.dart

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

Nối đoạn 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 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 main.dart. Thêm AppBarMenuButton để hiển thị tùy 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ẽ có thể nhìn thấy menu ngữ cảnh 71fcc1030a336423.png ở phía bên phải của thanh ứng dụng. Nhấp vào nó sẽ đưa bạn đến 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ẽ có thể thấy tùy chọn Đăng xuất trong menu ngữ cảnh.

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

888506c86a28a72c.png

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 đăng ký thiết bị bằng API 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 Firebase,

  1. Điều hướng đến phần Cơ sở dữ liệu thời gian thực của bảng điều khiển 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 của bạn, hãy chọn Chế độ kiểm tra ngay bây giờ**.** (Chế độ kiểm tra tạo Quy tắc bảo mật cho phép tất cả các yêu cầu được thông qua. Bạn sẽ thêm Quy tắc bảo mật sau. Điều quan trọng là không bao giờ đưa vào sản xuất với Quy tắc bảo mật của bạn vẫn ở Chế độ thử nghiệm.)

Cơ sở dữ liệu hiện đang trống. Xác định databaseURL của bạn trong Cài đặt dự án , trong tab Chung . Cuộ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 chúng xuất hiện trực tuyến. Để thực hiện việc này, bạn sẽ tận dụng Cài đặt Firebase và API hiện diện Firebase RTDB để theo dõi danh sách các thiết bị trực tuyến từ một người dùng. Đoạn mã sau sẽ giúp hoàn thành 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, xây dựng và chạy ứng dụng trên thiết bị của bạn hoặc trong trình duyệt có flutter run.

Trong ứng dụng của bạn, hãy đăng nhập với tư cách người dùng. Hãy nhớ đăng nhập với 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 Firebase , bạn sẽ thấy thiết bị của mình hiển thị dưới một ID người dùng trong cơ sở dữ liệu của bạn.

5bef49cea3564248.png

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

Chọn thiết bị dẫn

Để đồng bộ hóa 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ị dẫn đầu hoặc bộ điều khiển. Thiết bị dẫn đầu sẽ ra lệnh cho các trạng thái trên thiết bị theo dõi.

Tạo phương thức setLeadDevice trong application_state.dart và theo dõi thiết bị này bằng khóa 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 menu ngữ cảnh của thanh ứng dụng, hãy tạo PopupMenuItem có tên là Controller bằng cách sửa đổi tiện ích SignedInMenuButton . Menu này sẽ cho phép người dùng thiết lập thiết bị dẫn đầu.

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ị dẫn vào cơ sở dữ liệu

Sau khi đặt thiết bị dẫn, bạn có thể đồng bộ hóa trạng thái của thiết bị dẫn với RTDB bằng mã sau. Nối đoạn 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');
      }
    }
  }

Và 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ị dẫn từ cơ sở dữ liệu

Có hai phần để đọc và sử dụng trạng thái của thiết bị dẫn. Trước tiên, bạn muốn thiết lập trình xử lý cơ sở dữ liệu về trạng thái trình phát chính trong application_state . Trình nghe này sẽ thông báo cho các thiết bị theo dõi khi nào cần cập nhật màn hình thông qua lệnh gọi lại. Lưu ý rằng bạn đã xác định giao diện OnLeadDeviceChangeCallback trong bước này. Nó 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, khởi động 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 để có thể cập nhật trạng thái trình phát của người theo dõi 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');
        }
      }
    }
  }

Bây giờ bạn đã sẵn sàng để thử nghiệm ứng dụng:

  1. Trên dòng lệnh, 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 trình duyệt, trên trình mô phỏng iOS hoặc trình mô phỏng Android. Chuyển đến menu ngữ cảnh, chọn một ứng dụng làm thiết bị dẫn đầu. Bạn sẽ có thể thấy trình phát của thiết bị theo dõ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ị dẫn đầu, phát hoặc tạm dừng nhạc và quan sát các thiết bị theo dõi cập nhật tương ứng.

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

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

Trừ khi chúng tôi viết các quy tắc bảo mật tốt hơn, nếu không ai đó có thể viết trạng thái cho thiết bị mà họ không sở hữu! Vì vậy, trước khi bạn hoàn tất, hãy cập nhật Quy tắc bảo mật cơ sở dữ liệu thời gian thực để đảm bảo người dùng duy nhất có thể đọc hoặc ghi vào thiết bị là người dùng đã đăng nhập vào thiết bị đó. Trong Bảng điều khiển Firebase, hãy điều hướng đến Cơ sở dữ liệu thời gian thực và sau đó đến tab Quy tắc . Dán các quy tắc sau 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 đã xây dựng thành công bộ điều khiển từ xa cho nhiều thiết bị bằng Flutter!

Tín dụng

Cùng nhau tốt đẹp hơn, một bài hát của Firebase

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

9. Tiền thưởng

Là một thách thức bổ sung, 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 hỗ trợ, nó sẽ được triển khai trong thư mục chứa trạng thái hoàn thà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