1. 소개
최종 업데이트: 2022년 3월 14일
교차 기기 통신을 위한 FlutterFire
홈 자동화, 웨어러블, 개인 건강 기술 기기가 온라인으로 연결되는 수가 많아짐에 따라 교차 기기 통신이 모바일 애플리케이션 빌드의 중요한 부분이 되고 있습니다. 휴대전화 앱에서 브라우저를 제어하거나 휴대전화에서 TV에서 재생되는 콘텐츠를 제어하는 등 교차 기기 통신을 설정하는 것은 일반적으로 일반적인 모바일 앱을 빌드하는 것보다 더 복잡합니다 .
Firebase의 실시간 데이터베이스는 사용자가 기기의 온라인/오프라인 상태를 확인할 수 있는 Presence API 를 제공합니다. 이 API를 Firebase 설치 서비스와 함께 사용하여 동일한 사용자가 로그인한 모든 기기를 추적하고 연결합니다. Flutter를 사용하여 여러 플랫폼용 애플리케이션을 빠르게 만든 다음, 한 기기에서 음악을 재생하고 다른 기기에서 음악을 제어하는 교차 기기 프로토타입을 빌드합니다.
빌드할 항목
이 Codelab에서는 간단한 음악 플레이어 리모컨을 빌드합니다. 이 앱에는 아래의 기능이 있습니다.
- Flutter로 빌드된 Android, iOS, 웹의 간단한 음악 플레이어가 있습니다.
- 사용자가 로그인하도록 허용합니다.
- 동일한 사용자가 여러 기기에 로그인한 경우 기기를 연결합니다.
- 사용자가 한 기기에서 다른 기기의 음악 재생을 제어할 수 있도록 허용
학습 내용
- 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 Emulator 또는 iOS 시뮬레이터 설정
개발용 Android 기기 또는 iOS 기기가 이미 있는 경우 이 단계를 건너뛸 수 있습니다.
Android 에뮬레이터를 만들려면 Flutter 개발도 지원하는 Android 스튜디오를 다운로드하고 가상 기기 만들기 및 관리의 안내를 따르세요.
iOS 시뮬레이터를 만들려면 Mac 환경이 필요합니다. XCode를 다운로드하고 시뮬레이터 개요 > 시뮬레이터 사용 > 시뮬레이터 열기 및 닫기의 안내를 따릅니다.
3. Firebase 설정하기
Firebase 프로젝트 만들기
- Google 계정을 사용하여 Firebase Console에 로그인합니다.
- 버튼을 클릭하여 새 프로젝트를 만든 다음 프로젝트 이름 (예:
Firebase-Cross-Device-Codelab
)을 입력합니다.
- 계속을 클릭합니다.
- 메시지가 표시되면 Firebase 약관을 검토하고 이에 동의한 다음 계속을 클릭합니다.
- (선택사항) Firebase Console에서 AI 지원('Firebase의 Gemini'라고 함)을 사용 설정합니다.
- 이 Codelab에서는 Google 애널리틱스가 필요하지 않으므로 Google 애널리틱스 옵션을 사용 중지합니다.
- 프로젝트 만들기를 클릭하고 프로젝트가 프로비저닝될 때까지 기다린 다음 계속을 클릭합니다.
Firebase SDK 설치
명령줄의 프로젝트 디렉터리에서 다음 명령어를 실행하여 Firebase를 설치합니다.
flutter pub add firebase_core
pubspec.yaml
파일에서 firebase_core
의 버전을 1.13.1 이상으로 수정하거나 flutter upgrade
를 실행합니다.
FlutterFire 초기화
- Firebase CLI가 설치되어 있지 않으면
curl -sL https://firebase.tools | bash
를 실행하여 설치할 수 있습니다. firebase login
을 실행하고 안내를 따라 로그인합니다.dart pub global activate flutterfire_cli
을 실행하여 FlutterFire CLI를 설치합니다.flutterfire configure
를 실행하여 FlutterFire CLI를 구성합니다.- 프롬프트에서 이 Codelab을 위해 방금 만든 프로젝트(예: Firebase-Cross-Device-Codelab)를 선택합니다.
- 구성 지원을 선택하라는 메시지가 표시되면 iOS, Android, Web을 선택합니다.
- Apple 번들 ID를 묻는 메시지가 표시되면 고유한 도메인을 입력하거나 이 Codelab의 목적에 적합한
com.example.appname
를 입력합니다.
구성되면 초기화에 필요한 모든 옵션이 포함된 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 인증에 이메일 로그인 사용 설정
사용자가 웹 앱에 로그인할 수 있도록 이메일/비밀번호 로그인 방법을 사용합니다.
- Firebase Console의 왼쪽 패널에서 빌드 메뉴를 펼칩니다.
- 인증을 클릭한 다음 시작하기 버튼과 로그인 방법 탭을 차례로 클릭합니다.
- 로그인 제공업체 목록에서 이메일/비밀번호를 클릭하고 사용 설정 스위치를 켬 위치로 설정한 다음 저장을 클릭합니다.
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는 동일하게 유지되어야 하지만 이제 사용자가 로그인하고 앱 상태를 저장할 수 있습니다.
로그인 흐름 만들기
이 단계에서는 로그인 및 로그아웃 흐름을 작업합니다. 흐름은 다음과 같습니다.
- 로그아웃한 사용자는 앱 바의 오른쪽에 있는 컨텍스트 메뉴
를 클릭하여 로그인 절차를 시작합니다.
- 로그인 흐름이 대화상자에 표시됩니다.
- 사용자가 이전에 로그인한 적이 없는 경우 유효한 이메일 주소와 비밀번호를 사용하여 계정을 만들라는 메시지가 표시됩니다.
- 사용자가 이전에 로그인한 적이 있는 경우 비밀번호를 입력하라는 메시지가 표시됩니다.
- 사용자가 로그인하면 컨텍스트 메뉴를 클릭할 때 로그아웃 옵션이 표시됩니다.
로그인 흐름을 추가하려면 세 단계를 거쳐야 합니다.
먼저 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.
에서 기존 appBar 위젯을 찾아 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
명령어를 실행하여 이러한 변경사항을 적용한 상태로 앱을 다시 시작합니다. 앱 바 오른쪽에 컨텍스트 메뉴 가 표시됩니다. 이 버튼을 클릭하면 로그인 대화상자가 표시됩니다.
유효한 이메일 주소와 비밀번호로 로그인하면 컨텍스트 메뉴에 로그아웃 옵션이 표시됩니다.
Firebase Console의 인증에서 이메일 주소가 신규 사용자로 표시되는지 확인합니다.
수고하셨습니다. 이제 사용자가 앱에 로그인할 수 있습니다.
5. 데이터베이스 연결 추가
이제 Firebase Presence API를 사용하여 기기를 등록할 준비가 되었습니다.
명령줄에서 다음 명령어를 실행하여 필요한 종속 항목을 추가합니다.
flutter pub add firebase_app_installations
flutter pub add firebase_database
데이터베이스 만들기
Firebase Console에서
- Firebase Console의 실시간 데이터베이스 섹션으로 이동합니다. 데이터베이스 만들기를 클릭합니다.
- 보안 규칙의 시작 모드를 선택하라는 메시지가 표시되면 지금은 테스트 모드를 선택합니다**.** (테스트 모드에서는 모든 요청을 허용하는 보안 규칙이 생성됩니다. 보안 규칙은 나중에 추가합니다. 보안 규칙이 테스트 모드인 상태로 프로덕션에 배포해서는 안 됩니다.)
현재 데이터베이스가 비어 있습니다. 프로젝트 설정의 일반 탭에서 databaseURL
을 찾습니다. 웹 앱 섹션까지 아래로 스크롤합니다.
firebase_options.dart
파일에 databaseURL
추가:
lib/firebase_options.dart
static const FirebaseOptions web = FirebaseOptions(
apiKey: yourApiKey,
...
databaseURL: 'https://<YOUR_DATABASE_URL>,
...
);
RTDB Presence API를 사용하여 기기 등록
사용자가 온라인 상태가 되면 사용자의 기기를 등록하려고 합니다. 이를 위해 Firebase Installations 및 Firebase RTDB Presence API를 활용하여 단일 사용자의 온라인 기기 목록을 추적합니다. 다음 코드를 사용하면 이 목표를 달성할 수 있습니다.
lib/src/application_state.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart';
class ApplicationState extends ChangeNotifier {
String? _deviceId;
String? _uid;
Future<void> init() async {
...
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
_uid = user.uid;
_addUserDevice();
}
...
});
}
Future<void> _addUserDevice() async {
_uid = FirebaseAuth.instance.currentUser?.uid;
String deviceType = _getDevicePlatform();
// Create two objects which we will write to the
// Realtime database when this device is offline or online
var isOfflineForDatabase = {
'type': deviceType,
'state': 'offline',
'last_changed': ServerValue.timestamp,
};
var isOnlineForDatabase = {
'type': deviceType,
'state': 'online',
'last_changed': ServerValue.timestamp,
};
var devicesRef =
FirebaseDatabase.instance.ref().child('/users/$_uid/devices');
FirebaseInstallations.instance
.getId()
.then((id) => _deviceId = id)
.then((_) {
// Use the semi-persistent Firebase Installation Id to key devices
var deviceStatusRef = devicesRef.child('$_deviceId');
// RTDB Presence API
FirebaseDatabase.instance
.ref()
.child('.info/connected')
.onValue
.listen((data) {
if (data.snapshot.value == false) {
return;
}
deviceStatusRef.onDisconnect().set(isOfflineForDatabase).then((_) {
deviceStatusRef.set(isOnlineForDatabase);
});
});
});
}
String _getDevicePlatform() {
if (kIsWeb) {
return 'Web';
} else if (Platform.isIOS) {
return 'iOS';
} else if (Platform.isAndroid) {
return 'Android';
}
return 'Unknown';
}
명령줄로 돌아가서 flutter run.
를 사용하여 기기 또는 브라우저에서 앱을 빌드하고 실행합니다.
앱에서 사용자로 로그인합니다. 다른 플랫폼에서 동일한 사용자로 로그인해야 합니다.
Firebase Console에서 데이터베이스의 한 사용자 ID 아래에 기기가 표시됩니다.
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');
}
}
}
}
이제 앱을 테스트할 준비가 되었습니다.
- 명령줄에서
flutter run -d <device-name>
를 사용하여 에뮬레이터 또는 브라우저에서 앱을 실행합니다. - 브라우저, iOS 시뮬레이터 또는 Android 에뮬레이터에서 앱을 엽니다. 컨텍스트 메뉴로 이동하여 리더 기기로 사용할 앱을 선택합니다. 리더 기기가 업데이트되면 팔로어 기기의 플레이어가 변경되는 것을 확인할 수 있습니다.
- 이제 리더 기기를 변경하고 음악을 재생하거나 일시중지한 다음 팔로어 기기가 그에 따라 업데이트되는지 확인합니다.
팔로워 기기가 제대로 업데이트되면 교차 기기 컨트롤러를 만드는 데 성공한 것입니다. 이제 중요한 단계 하나만 남았습니다.
7. 보안 규칙 업데이트
더 나은 보안 규칙을 작성하지 않으면 소유하지 않은 기기에 상태를 쓸 수 있습니다. 따라서 완료하기 전에 실시간 데이터베이스 보안 규칙을 업데이트하여 기기를 읽거나 쓸 수 있는 사용자가 해당 기기에 로그인한 사용자뿐인지 확인합니다. Firebase Console에서 실시간 데이터베이스로 이동한 다음 규칙 탭으로 이동합니다. 로그인한 사용자만 자신의 기기 상태를 읽고 쓸 수 있도록 허용하는 다음 규칙을 붙여넣습니다.
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. 수고하셨습니다.
수고하셨습니다. Flutter를 사용하여 교차 기기 원격 컨트롤러를 빌드했습니다.
크레딧
Better Together, a Firebase Song
- 음악: 라이언 버넌
- 가사와 앨범 커버는 마리사 크리스티가 맡았습니다.
- JP Gomez의 음성
9. 보너스
추가 과제로 Flutter FutureBuilder
를 사용하여 현재 리드 기기 유형을 UI에 비동기식으로 추가해 보세요. 도움이 필요한 경우 Codelab의 완료된 상태가 포함된 폴더에 구현되어 있습니다.