1. 소개
최종 업데이트: 2022년 3월 14일
기기 간 통신을 위한 FlutterFire
수많은 홈 자동화, 웨어러블, 개인 건강 관리 기술 기기가 온라인에 등장함에 따라 교차 기기 통신이 모바일 애플리케이션을 빌드하는 데 점점 더 중요한 부분이 되고 있습니다. 휴대전화 앱에서 브라우저를 제어하거나 휴대전화에서 TV에서 재생되는 콘텐츠를 제어하는 등 교차 기기 통신을 설정하는 것은 일반적으로 일반 모바일 앱을 빌드하는 것보다 더 복잡합니다.
Firebase의 실시간 데이터베이스는 사용자가 기기의 온라인/오프라인 상태를 볼 수 있는 Presence 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 Emulator 또는 iOS 시뮬레이터를 잘 알고 있다면 flutter run -d <device_name>
명령어를 사용하여 해당 플랫폼용 앱을 빌드하고 설치할 수 있습니다.
웹 앱에 기본 독립형 음악 플레이어가 표시됩니다. 플레이어 기능이 의도한 대로 작동하는지 확인합니다. 이 앱은 이 Codelab을 위해 설계된 간단한 음악 플레이어 앱입니다. Better Together Firebase 노래만 재생할 수 있습니다.
Android Emulator 또는 iOS 시뮬레이터 설정
이미 개발용 Android 기기 또는 iOS 기기가 있는 경우 이 단계를 건너뛸 수 있습니다.
Android Emulator를 만들려면 Flutter 개발도 지원하는 Android 스튜디오를 다운로드하고 가상 기기 만들기 및 관리의 안내를 따르세요.
iOS 시뮬레이터를 만들려면 Mac 환경이 필요합니다. XCode를 다운로드하고 시뮬레이터 개요 > 시뮬레이터 사용 > 시뮬레이터 열기 및 닫기의 안내를 따릅니다.
3. Firebase 설정
Firebase 프로젝트 만들기
브라우저를 열어 http://console.firebase.google.com/으로 이동합니다.
- Firebase에 로그인합니다.
- Firebase Console에서 프로젝트 추가 (또는 프로젝트 만들기)를 클릭하고 Firebase 프로젝트의 이름을 Firebase-Cross-Device-Codelab으로 지정합니다.
- 프로젝트 만들기 옵션을 클릭하며 살펴봅니다. 메시지가 표시되면 Firebase 약관에 동의합니다. 이 앱에는 애널리틱스를 사용하지 않으므로 Google 애널리틱스 설정을 건너뜁니다.
언급된 파일을 다운로드하거나 build.gradle 파일을 변경할 필요가 없습니다. FlutterFire를 초기화할 때 이를 구성합니다.
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, 웹을 선택합니다.
- Apple 번들 ID를 묻는 메시지가 표시되면 고유한 도메인을 입력하거나
com.example.appname
을 입력합니다. 이 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
를 찾습니다. 웹 앱 섹션으로 스크롤합니다.
databaseURL
를 firebase_options.dart
파일에 추가합니다.:
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 Console에서 데이터베이스의 사용자 ID 1개로 기기가 표시됩니다.
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 Emulator에서 앱을 엽니다. 컨텍스트 메뉴로 이동하여 리더 기기가 될 앱을 하나 선택합니다. 팔로어 기기의 플레이어가 바뀐다는 것을 알 수 있습니다.
- 이제 리더 기기를 변경하고 음악을 재생하거나 일시중지한 다음 이에 따라 업데이트되는 후속 기기를 관찰합니다.
팔로어 기기가 올바르게 업데이트되면 교차 기기 컨트롤러를 성공적으로 만든 것입니다. 이제 중요한 한 단계만 남았습니다.
7. 보안 규칙 업데이트
더 나은 보안 규칙을 작성하지 않으면 누군가가 자신이 소유하지 않은 기기에 상태를 쓸 수 있습니다. 따라서 작업을 완료하기 전에 해당 기기에 로그인한 사용자만 기기를 읽거나 쓸 수 있도록 실시간 데이터베이스 보안 규칙을 업데이트합니다. Firebase Console에서 실시간 데이터베이스와 규칙 탭으로 차례로 이동합니다. 로그인한 사용자만 자신의 기기 상태를 읽고 쓸 수 있도록 허용하는 다음 규칙을 붙여넣습니다.
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. 수고하셨습니다.
축하합니다. Flutter를 사용하여 교차 기기 원격 컨트롤러를 빌드했습니다.
크레딧
함께하는 Firebase 노래
- 라이언 버넌이 음악
- 마리사 크리스티 작사 및 앨범 커버
- JP Gomez의 음성
9. 보너스
또 다른 과제로는 Flutter FutureBuilder
를 사용하여 현재 주요 기기 유형을 UI에 비동기식으로 추가하는 것입니다. 도움이 필요한 경우 Codelab의 완료 상태가 포함된 폴더에 구현됩니다.