Firebase 교차 기기 Codelab

1. 소개

최종 업데이트 날짜: 2022-03-14

기기 간 통신을 위한 FlutterFire

수많은 홈 자동화, 웨어러블 및 개인 건강 기술 장치가 온라인에 등장하면서 장치 간 통신이 모바일 애플리케이션 구축에서 점점 더 중요한 부분이 되고 있습니다. 휴대폰 앱에서 브라우저를 제어하거나 휴대폰에서 TV에서 재생되는 콘텐츠를 제어하는 ​​등 기기 간 통신을 설정하는 것은 일반적으로 일반 모바일 앱을 구축하는 것보다 더 복잡합니다.

Firebase의 실시간 데이터베이스는 사용자가 장치의 온라인/오프라인 상태를 확인할 수 있는 Presence API를 제공합니다. 이를 Firebase 설치 서비스와 함께 사용하여 동일한 사용자가 로그인한 모든 기기를 추적하고 연결합니다. Flutter를 사용하여 여러 플랫폼용 애플리케이션을 빠르게 만든 다음 재생되는 교차 기기 프로토타입을 구축합니다. 한 기기에서는 음악을 듣고 다른 기기에서는 음악을 제어할 수 있습니다!

무엇을 구축할 것인가

이 Codelab에서는 간단한 음악 플레이어 리모컨을 빌드합니다. 귀하의 앱은 다음을 수행합니다.

  • Flutter로 구축된 Android, iOS, 웹에서 간단한 음악 플레이어를 만들어 보세요.
  • 사용자가 로그인할 수 있도록 허용합니다.
  • 동일한 사용자가 여러 장치에 로그인되어 있는 경우 장치를 연결하세요.
  • 사용자가 한 기기의 음악 재생을 다른 기기에서 제어할 수 있도록 허용합니다.

7f0279938e1d3ab5.gif

무엇을 배울 것인가

  • Flutter 음악 플레이어 앱을 빌드하고 실행하는 방법.
  • 사용자가 Firebase 인증으로 로그인하도록 허용하는 방법
  • Firebase RTDB Presence API 및 Firebase 설치 서비스를 사용하여 기기를 연결하는 방법입니다.

필요한 것

  • Flutter 개발 환경. Flutter 설치 가이드 의 지침에 따라 설정하세요.
  • 최소 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> 방문하세요.

Android 에뮬레이터 또는 iOS 시뮬레이터에 익숙하다면 해당 플랫폼용 앱을 빌드하고 flutter run -d <device_name> 명령을 사용하여 설치할 수 있습니다.

웹 앱은 기본 독립형 음악 플레이어를 표시해야 합니다. 플레이어 기능이 의도한 대로 작동하는지 확인하세요. 이 Codelab을 위해 설계된 간단한 음악 플레이어 앱입니다. Firebase 노래인 Better Together 만 재생할 수 있습니다.

Android 에뮬레이터 또는 iOS 시뮬레이터 설정

개발용 Android 기기 또는 iOS 기기가 이미 있는 경우 이 단계를 건너뛸 수 있습니다.

Android 에뮬레이터를 만들려면 Flutter 개발도 지원하는 Android Studio를 다운로드하고 가상 장치 만들기 및 관리 의 지침을 따르세요.

iOS 시뮬레이터를 만들려면 Mac 환경이 필요합니다. XCode를 다운로드하고 시뮬레이터 개요 > 시뮬레이터 사용 > 시뮬레이터 열기 및 닫기 의 지침을 따르세요.

3. 파이어베이스 설정

Firebase 프로젝트 만들기

브라우저에서 http://console.firebase.google.com/ 을 엽니다.

  1. Firebase 에 로그인합니다.
  2. Firebase 콘솔에서 프로젝트 추가 (또는 프로젝트 만들기 )를 클릭하고 Firebase 프로젝트 이름을 Firebase-Cross-Device-Codelab 으로 지정합니다.
  3. 프로젝트 생성 옵션을 클릭하세요. 메시지가 표시되면 Firebase 약관에 동의하세요. 이 앱에서는 Analytics를 사용하지 않으므로 Google 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 를 실행하여 설치할 수 있습니다. curl -sL https://firebase.tools | bash .
  2. firebase login 실행하고 프롬프트에 따라 로그인합니다.
  3. dart pub global activate flutterfire_cli 실행하여 FlutterFire CLI를 설치하세요.
  4. flutterfire configure 실행하여 FlutterFire CLI를 구성합니다.
  5. 프롬프트에서 이 Codelab을 위해 방금 만든 프로젝트(예: Firebase-Cross-Device-Codelab) 를 선택합니다.
  6. 구성 지원을 선택하라는 메시지가 나타나면 iOS , Android웹을 선택하세요.
  7. Apple 번들 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

아직 UI ​​요소를 변경하지 않았으므로 앱의 모양과 동작도 변경되지 않았습니다. 하지만 이제 Firebase 앱이 있으므로 다음을 포함한 Firebase 제품을 사용할 수 있습니다.

  • 사용자가 앱에 로그인할 수 있도록 하는 Firebase 인증 .
  • Firebase 실시간 데이터베이스(RTDB) ; 현재 상태 API를 사용하여 장치의 온라인/오프라인 상태를 추적합니다.
  • Firebase 보안 규칙을 사용 하면 데이터베이스를 보호할 수 있습니다.
  • 단일 사용자가 로그인한 기기를 식별하는 Firebase 설치 서비스입니다 .

4. Firebase 인증 추가

Firebase 인증을 위한 이메일 로그인 활성화

사용자가 웹 앱에 로그인할 수 있도록 하려면 이메일/비밀번호 로그인 방법을 사용합니다.

  1. Firebase 콘솔의 왼쪽 패널에서 빌드 메뉴를 확장합니다.
  2. 인증 을 클릭한 다음 시작하기 버튼을 클릭하고 로그인 방법 탭을 클릭합니다.
  3. 로그인 공급자 목록에서 이메일/비밀번호를 클릭하고 활성화 스위치를 켜짐 위치로 설정한 다음 저장 을 클릭합니다. 58e3e3e23c2f16a4.png

Flutter에서 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(),
  ));
}

다시 말하지만 애플리케이션 UI는 동일하게 유지되어야 하지만 이제 사용자가 로그인하고 앱 상태를 저장할 수 있습니다.

로그인 흐름 만들기

이 단계에서는 로그인 및 로그아웃 흐름을 작업하게 됩니다. 흐름은 다음과 같습니다.

  1. 로그아웃한 사용자는 상황에 맞는 메뉴를 클릭하여 로그인 흐름을 시작합니다. 71fcc1030a336423.png 앱바 오른쪽에 있습니다.
  2. 로그인 흐름이 대화상자에 표시됩니다.
  3. 사용자가 이전에 로그인한 적이 없는 경우 유효한 이메일 주소와 비밀번호를 사용하여 계정을 생성하라는 메시지가 표시됩니다.
  4. 사용자가 이전에 로그인한 경우 비밀번호를 입력하라는 메시지가 표시됩니다.
  5. 사용자가 로그인한 후 상황에 맞는 메뉴를 클릭하면 로그아웃 옵션이 표시됩니다.

c295f6fa2e1d40f3.png

로그인 흐름을 추가하려면 세 단계가 필요합니다.

먼저 AppBarMenuButton 위젯을 생성합니다. 이 위젯은 사용자의 loginState 에 따라 컨텍스트 메뉴 팝업을 제어합니다. 가져오기 추가

lib/src/widgets.dart

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

widgets.dart.

lib/src/widgets.dart

class AppBarMenuButton extends StatelessWidget {
  const AppBarMenuButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
      builder: (context, appState, child) {
        if (appState.loginState == ApplicationLoginState.loggedIn) {
          return SignedInMenuButton(buildContext: context);
        }
        return SignInMenuButton(buildContext: context);
      },
    );
  }
}

class SignedInMenuButton extends StatelessWidget {
  const SignedInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      onSelected: _handleSignedInMenu,
      color: Colors.deepPurple.shade300,
      itemBuilder: (context) => _getMenuItemBuilder(),
    );
  }

  List<PopupMenuEntry<String>> _getMenuItemBuilder() {
    return [
      const PopupMenuItem<String>(
        value: 'Sign out',
        child: Text(
          'Sign out',
          style: TextStyle(color: Colors.white),
        ),
      )
    ];
  }

  Future<void> _handleSignedInMenu(String value) async {
    switch (value) {
      case 'Sign out':
        Provider.of<ApplicationState>(buildContext, listen: false).signOut();
        break;
    }
  }
}

class SignInMenuButton extends StatelessWidget {
  const SignInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      onSelected: _signIn,
      color: Colors.deepPurple.shade300,
      itemBuilder: (context) => _getMenuItemBuilder(context),
    );
  }

  Future<void> _signIn(String value) async {
    return showDialog<void>(
      context: buildContext,
      builder: (context) => const SignInDialog(),
    );
  }

  List<PopupMenuEntry<String>> _getMenuItemBuilder(BuildContext context) {
    return [
      const PopupMenuItem<String>(
        value: 'Sign in',
        child: Text(
          'Sign in',
          style: TextStyle(color: Colors.white),
        ),
      ),
    ];
  }
}

둘째, 동일한 widgets.dart 클래스에서 SignInDialog 위젯을 만듭니다.

lib/src/widgets.dart

class SignInDialog extends AlertDialog {
  const SignInDialog({Key? key}) : super(key: key);

  @override
  AlertDialog build(BuildContext context) {
    return AlertDialog(
      content: Column(mainAxisSize: MainAxisSize.min, children: [
        Consumer<ApplicationState>(
          builder: (context, appState, _) => Authentication(
            email: appState.email,
            loginState: appState.loginState,
            startLoginFlow: appState.startLoginFlow,
            verifyEmail: appState.verifyEmail,
            signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
            cancelRegistration: appState.cancelRegistration,
            registerAccount: appState.registerAccount,
            signOut: appState.signOut,
          ),
        ),
      ]),
    );
  }
}

셋째, main.dart. 로그인 또는 로그아웃 옵션을 표시하려면 AppBarMenuButton 추가하세요.

lib/main.dart

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

이러한 변경 사항으로 앱을 다시 시작하려면 flutter run 명령을 실행하세요. 상황에 맞는 메뉴를 볼 수 있어야합니다 71fcc1030a336423.png 앱바 오른쪽에 있습니다. 클릭하면 로그인 대화상자로 이동합니다.

유효한 이메일 주소와 비밀번호로 로그인하면 상황에 맞는 메뉴에 로그아웃 옵션이 표시됩니다.

Firebase 콘솔의 인증 아래에서 새 사용자로 나열된 이메일 주소를 볼 수 있습니다.

888506c86a28a72c.png

축하해요! 이제 사용자는 앱에 로그인할 수 있습니다!

5. 데이터베이스 연결 추가

이제 Firebase Presence API를 사용하여 기기 등록을 진행할 준비가 되었습니다.

명령줄에서 다음 명령을 실행하여 필요한 종속성을 추가합니다.

flutter pub add firebase_app_installations

flutter pub add firebase_database

데이터베이스 만들기

Firebase 콘솔에서는

  1. Firebase 콘솔실시간 데이터베이스 섹션으로 이동합니다. 데이터베이스 생성 을 클릭합니다.
  2. 보안 규칙의 시작 모드를 선택하라는 메시지가 표시되면 지금은 테스트 모드를 선택하세요**.** (테스트 모드는 모든 요청을 허용하는 보안 규칙을 생성합니다. 보안 규칙은 나중에 추가할 것입니다. 보안 규칙은 여전히 ​​테스트 모드에 있습니다.)

지금은 데이터베이스가 비어 있습니다. 일반 탭 아래의 프로젝트 설정 에서 databaseURL 찾으세요. 웹 앱 섹션까지 아래로 스크롤합니다.

1b6076f60a36263b.png

firebase_options.dart 파일에 databaseURL 추가하세요 :

lib/firebase_options.dart

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

RTDB Presence API를 사용하여 장치 등록

사용자의 장치가 온라인에 나타날 때 이를 등록하려고 합니다. 이를 위해 Firebase 설치 및 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 콘솔 에서 데이터베이스의 하나의 사용자 ID 아래에 장치가 표시되는 것을 볼 수 있습니다.

5bef49cea3564248.png

6. 장치 상태 동기화

리드 장치 선택

장치 간 상태를 동기화하려면 한 장치를 리더 또는 컨트롤러로 지정하세요. 리드 장치는 팔로어 장치의 상태를 결정합니다.

application_state.dart 에서 setLeadDevice 메소드를 생성하고 RTDB에서 active_device 키를 사용하여 이 장치를 추적합니다.

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 위젯을 수정하여 Controller 라는 PopupMenuItem 생성하세요. 이 메뉴를 통해 사용자는 리드 장치를 설정할 수 있습니다.

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 시뮬레이터 또는 Android 에뮬레이터에서 앱을 엽니다. 상황에 맞는 메뉴로 이동하여 리더 장치가 될 앱 하나를 선택하세요. 리더 장치가 업데이트됨에 따라 팔로어 장치의 플레이어가 변경되는 것을 볼 수 있어야 합니다.
  3. 이제 리더 장치를 변경하고, 음악을 재생하거나 일시 정지하고, 이에 따라 팔로어 장치가 업데이트되는 것을 관찰하세요.

팔로어 장치가 제대로 업데이트되면 크로스 장치 컨트롤러 만들기에 성공한 것입니다. 이제 중요한 단계가 하나 남았습니다.

7. 보안 규칙 업데이트

더 나은 보안 규칙을 작성하지 않으면 누군가 자신이 소유하지 않은 장치에 상태를 쓸 수 있습니다! 따라서 완료하기 전에 실시간 데이터베이스 보안 규칙을 업데이트하여 해당 장치에 로그인한 사용자만 장치를 읽거나 쓸 수 있도록 하십시오. Firebase 콘솔에서 실시간 데이터베이스로 이동한 다음 규칙 탭으로 이동합니다. 로그인한 사용자만 자신의 장치 상태를 읽고 쓸 수 있도록 다음 규칙을 붙여넣습니다.

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

8. 축하합니다!

bcd986f7106d892b.gif

축하합니다. Flutter를 사용하여 교차 장치 원격 컨트롤러를 성공적으로 구축했습니다!

크레딧

Better Together, Firebase 노래

  • 음악: 라이언 버논
  • Marissa Christy의 가사 및 앨범 커버
  • JP 고메즈의 목소리

9. 보너스

추가 과제로 Flutter FutureBuilder 사용하여 현재 리드 장치 유형을 UI에 비동기식으로 추가하는 것을 고려해 보세요. 도움이 필요한 경우 Codelab의 완료 상태가 포함된 폴더에 구현됩니다.

참조 문서 및 다음 단계