1. บทนำ
อัปเดตล่าสุด 14-03-2022
FlutterFire สำหรับการสื่อสารระหว่างอุปกรณ์
เมื่อเห็นว่าการทำงานอัตโนมัติในบ้าน อุปกรณ์ที่สวมใส่ได้ และอุปกรณ์เทคโนโลยีด้านสุขภาพส่วนบุคคลจำนวนมากเริ่มเข้าสู่โลกออนไลน์ การสื่อสารข้ามอุปกรณ์จึงกลายเป็นส่วนสำคัญมากขึ้นเรื่อยๆ ในการสร้างแอปพลิเคชันบนอุปกรณ์เคลื่อนที่ การตั้งค่าการสื่อสารระหว่างอุปกรณ์ เช่น การควบคุมเบราว์เซอร์จากแอปโทรศัพท์ หรือการควบคุมสิ่งที่เล่นบนทีวีจากโทรศัพท์ มักจะซับซ้อนกว่าการสร้างแอปบนอุปกรณ์เคลื่อนที่ทั่วไป
Realtime Database ของ Firebase มี Presence API ซึ่งช่วยให้ผู้ใช้ดูสถานะออนไลน์/ออฟไลน์ของอุปกรณ์ได้ คุณจะใช้บัญชีนี้กับบริการการติดตั้ง Firebase เพื่อติดตามและเชื่อมต่ออุปกรณ์ทั้งหมดที่ผู้ใช้รายเดียวกันลงชื่อเข้าใช้ คุณจะใช้ Flutter สร้างแอปพลิเคชันสำหรับหลายแพลตฟอร์มได้อย่างรวดเร็ว และสร้างต้นแบบข้ามอุปกรณ์ที่เปิดเพลงในอุปกรณ์หนึ่งและควบคุมเพลงในอุปกรณ์อื่น
สิ่งที่คุณจะสร้าง
ใน Codelab นี้ คุณจะได้สร้างรีโมตคอนโทรลเลอร์โปรแกรมเล่นเพลงแบบง่าย แอปของคุณจะ
- มีโปรแกรมเล่นเพลงที่ใช้งานง่ายบน Android, iOS และเว็บที่สร้างด้วย Flutter
- อนุญาตให้ผู้ใช้ลงชื่อเข้าใช้
- เชื่อมต่ออุปกรณ์เมื่อผู้ใช้รายเดียวกันลงชื่อเข้าใช้ในอุปกรณ์หลายเครื่อง
- อนุญาตให้ผู้ใช้ควบคุมการเล่นเพลงในอุปกรณ์หนึ่งจากอุปกรณ์อื่น
สิ่งที่คุณจะได้เรียนรู้
- วิธีสร้างและเรียกใช้แอปโปรแกรมเล่นเพลง Flutter
- วิธีอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้ด้วย Firebase Auth
- วิธีใช้ Firebase RTDB Presence API และบริการติดตั้ง Firebase เพื่อเชื่อมต่ออุปกรณ์
สิ่งที่คุณต้องมี
- สภาพแวดล้อมในการพัฒนาซอฟต์แวร์ Flutter ทำตามวิธีการในคู่มือการติดตั้ง Flutter เพื่อตั้งค่า
- ต้องใช้ Flutter เวอร์ชัน 2.10 ขึ้นไปเป็นอย่างน้อย หากคุณใช้เวอร์ชันที่ต่ำกว่า ให้เรียกใช้
flutter upgrade.
- บัญชี Firebase
2. การตั้งค่า
รับรหัสเริ่มต้น
เราได้สร้างแอปโปรแกรมเล่นเพลงใน Flutter ไว้แล้ว โค้ดเริ่มต้นจะอยู่ในที่เก็บ Git หากต้องการเริ่มต้นใช้งาน ให้โคลนที่เก็บในบรรทัดคำสั่ง ย้ายไปไว้ในโฟลเดอร์ที่มีสถานะเริ่มต้น แล้วติดตั้งทรัพยากร Dependency ต่อไปนี้
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 นี้ แต่เล่นได้เฉพาะเพลง Better Together ใน Firebase
ตั้งค่าโปรแกรมจำลองของ Android หรือเครื่องมือจำลอง iOS
คุณข้ามขั้นตอนนี้ได้หากมีอุปกรณ์ Android หรือ iOS สำหรับการพัฒนาอยู่แล้ว
หากต้องการสร้างโปรแกรมจำลอง Android ให้ดาวน์โหลด Android Studio ซึ่งรองรับการพัฒนา Flutter ด้วย และทำตามวิธีการในหัวข้อสร้างและจัดการอุปกรณ์เสมือน
หากต้องการสร้างเครื่องจำลอง iOS คุณจะต้องมีสภาพแวดล้อมของ Mac ดาวน์โหลด XCode แล้วทำตามวิธีการในภาพรวมโปรแกรมจำลอง > ใช้เครื่องจำลอง > เปิดและปิดเครื่องจำลอง
3. การตั้งค่า Firebase
สร้างโปรเจ็กต์ Firebase
เปิดเบราว์เซอร์ไปที่ http://console.firebase.google.com/
- ลงชื่อเข้าใช้ Firebase
- ในคอนโซล Firebase ให้คลิกเพิ่มโปรเจ็กต์ (หรือสร้างโปรเจ็กต์) และตั้งชื่อโปรเจ็กต์ Firebase ของคุณว่า Firebase-cross-Device-Codelab
- คลิกตัวเลือกการสร้างโปรเจ็กต์ ยอมรับข้อกำหนดของ Firebase หากได้รับข้อความแจ้ง ข้ามการตั้งค่า Google Analytics เนื่องจากคุณจะไม่ใช้ Analytics สําหรับแอปนี้
คุณไม่จำเป็นต้องดาวน์โหลดไฟล์ที่กล่าวถึงหรือเปลี่ยนไฟล์ 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
แล้วทำตามข้อความแจ้ง - ติดตั้ง FlutterFire CLI โดยเรียกใช้
dart pub global activate flutterfire_cli
- กำหนดค่า FlutterFire CLI โดยเรียกใช้
flutterfire configure
- เมื่อมีข้อความแจ้ง ให้เลือกโปรเจ็กต์ที่คุณเพิ่งสร้างสำหรับ Codelab นี้ เช่น Firebase-cross-Device-Codelab
- เลือก iOS, Android และเว็บเมื่อระบบแจ้งให้เลือกการรองรับการกำหนดค่า
- เมื่อได้รับแจ้งให้ใส่ Apple Bundle 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 ซึ่งช่วยให้ผู้ใช้ลงชื่อเข้าใช้แอปได้
- ฐานข้อมูลเรียลไทม์(RTDB) ของ Firebase คุณจะใช้ API การตรวจหาบุคคลเพื่อติดตามสถานะออนไลน์/ออฟไลน์ของอุปกรณ์
- กฎการรักษาความปลอดภัยของ Firebase จะช่วยรักษาความปลอดภัยให้ฐานข้อมูล
- บริการติดตั้ง Firebase เพื่อระบุอุปกรณ์ที่ผู้ใช้รายเดียวลงชื่อเข้าใช้
4. เพิ่มการตรวจสอบสิทธิ์ Firebase
เปิดใช้การลงชื่อเข้าใช้อีเมลสำหรับการตรวจสอบสิทธิ์ Firebase
หากต้องการอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้เว็บแอป คุณจะใช้วิธีการลงชื่อเข้าใช้แบบอีเมล/รหัสผ่าน ดังนี้
- ในคอนโซล Firebase ให้ขยายเมนูสร้างในแผงด้านซ้าย
- คลิกการตรวจสอบสิทธิ์ แล้วคลิกปุ่มเริ่มต้นใช้งาน จากนั้นคลิกแท็บวิธีการลงชื่อเข้าใช้
- คลิกอีเมล/รหัสผ่านในรายการผู้ให้บริการการลงชื่อเข้าใช้ แล้วเลื่อนสวิตช์เปิดใช้ไปที่ตำแหน่งเปิด แล้วคลิกบันทึก
กำหนดค่าการตรวจสอบสิทธิ์ Firebase ใน Flutter
ในบรรทัดคำสั่ง ให้เรียกใช้คำสั่งต่อไปนี้เพื่อติดตั้งแพ็กเกจ 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 ของแอปพลิเคชันควรเหมือนเดิม แต่ในตอนนี้คุณสามารถอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้และบันทึกสถานะแอปได้
สร้างขั้นตอนการลงชื่อเข้าใช้
ในขั้นตอนนี้ คุณจะทำงานกับขั้นตอนการลงชื่อเข้าใช้และออกจากระบบ ขั้นตอนจะมีลักษณะดังนี้
- ผู้ใช้ที่ออกจากระบบจะเริ่มขั้นตอนการลงชื่อเข้าใช้โดยคลิกที่เมนูตามบริบท ทางด้านขวาของแถบแอป
- ขั้นตอนการลงชื่อเข้าใช้จะแสดงในกล่องโต้ตอบ
- หากผู้ใช้ไม่เคยลงชื่อเข้าใช้มาก่อน ระบบจะแจ้งให้สร้างบัญชีโดยใช้ที่อยู่อีเมลและรหัสผ่านที่ถูกต้อง
- หากผู้ใช้เคยลงชื่อเข้าใช้มาก่อน ระบบจะแจ้งให้ป้อนรหัสผ่าน
- เมื่อผู้ใช้ลงชื่อเข้าใช้แล้ว การคลิกที่เมนูตามบริบทจะแสดงตัวเลือกออกจากระบบ
การเพิ่มขั้นตอนการลงชื่อเข้าใช้มี 3 ขั้นตอน
ก่อนอื่น ให้สร้างวิดเจ็ต 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),
),
),
];
}
}
ขั้นที่ 2 สร้างวิดเจ็ต SignInDialog
ในชั้นเรียน widgets.dart
เดียวกัน
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,
),
),
]),
);
}
}
ขั้นตอนที่ 3 ค้นหาวิดเจ็ต appBar ที่มีอยู่ใน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
เพื่อรีสตาร์ทแอปด้วยการเปลี่ยนแปลงเหล่านี้ คุณควรเห็นเมนูตามบริบท ทางด้านขวาของแถบแอป การคลิกที่ข้อความจะนำคุณไปยังกล่องโต้ตอบการลงชื่อเข้าใช้
เมื่อลงชื่อเข้าใช้ด้วยที่อยู่อีเมลและรหัสผ่านที่ถูกต้องแล้ว คุณควรจะเห็นตัวเลือกออกจากระบบในเมนูตามบริบท
ในคอนโซล Firebase ภายใต้การตรวจสอบสิทธิ์ คุณควรเห็นที่อยู่อีเมลแสดงเป็นผู้ใช้ใหม่ได้
ยินดีด้วย ตอนนี้ผู้ใช้ลงชื่อเข้าใช้แอปได้แล้ว
5. เพิ่มการเชื่อมต่อฐานข้อมูล
ตอนนี้คุณพร้อมที่จะไปลงทะเบียนอุปกรณ์โดยใช้ Firebase Presence API แล้ว
ในบรรทัดคำสั่ง ให้เรียกใช้คำสั่งต่อไปนี้เพื่อเพิ่มทรัพยากร Dependency ที่จำเป็น
flutter pub add firebase_app_installations
flutter pub add firebase_database
สร้างฐานข้อมูล
ในคอนโซล Firebase
- ไปที่ส่วนฐานข้อมูลเรียลไทม์ของคอนโซล Firebase คลิกสร้างฐานข้อมูล
- หากระบบแจ้งให้เลือกโหมดเริ่มต้นสำหรับกฎความปลอดภัย ให้เลือกโหมดทดสอบในตอนนี้**** (โหมดทดสอบจะสร้างกฎความปลอดภัยที่อนุญาตให้คำขอทั้งหมดผ่านได้ คุณจะต้องเพิ่มกฎความปลอดภัยในภายหลัง สิ่งสำคัญคือต้องไม่เป็นเวอร์ชันที่ใช้งานจริงโดยที่กฎความปลอดภัยยังคงอยู่ในโหมดทดสอบ)
ฐานข้อมูลว่างเปล่าในขณะนี้ ค้นหา 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 คุณควรเห็นอุปกรณ์แสดงอยู่ใต้รหัสผู้ใช้ในฐานข้อมูล
6. สถานะการซิงค์อุปกรณ์
เลือกอุปกรณ์สำหรับผู้มีโอกาสเป็นลูกค้า
หากต้องการซิงค์สถานะระหว่างอุปกรณ์ ให้กำหนดอุปกรณ์ 1 เครื่องเป็นผู้นำหรือตัวควบคุม อุปกรณ์โอกาสในการขายจะกำหนดสถานะในอุปกรณ์ของผู้ติดตาม
สร้างเมธอด setLeadDevice
ใน application_state.dart
และติดตามอุปกรณ์นี้ด้วยคีย์ active_device
ใน 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;
});
}
}
หากต้องการเพิ่มฟังก์ชันนี้ลงในเมนูตามบริบทของแถบแอป ให้สร้าง PopupMenuItem
ชื่อ Controller
โดยแก้ไขวิดเจ็ต SignedInMenuButton
เมนูนี้จะอนุญาตให้ผู้ใช้ตั้งค่าอุปกรณ์โอกาสในการขาย
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.
การดำเนินการนี้จะเริ่มจัดเก็บแอตทริบิวต์ 2 รายการ ได้แก่ สถานะของโปรแกรมเล่น (เล่นหรือหยุดชั่วคราว) และตำแหน่งแถบเลื่อน
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;
}
อ่านสถานะของอุปกรณ์โอกาสในการขายจากฐานข้อมูล
การอ่านและใช้สถานะของอุปกรณ์โอกาสในการขายมีอยู่ 2 ส่วน ก่อนอื่น คุณต้องตั้งค่า Listener ของฐานข้อมูลสถานะของเพลเยอร์โอกาสในการขายใน application_state
Listener รายนี้จะบอกอุปกรณ์ของผู้ติดตามว่าจะอัปเดตหน้าจอผ่าน Callback เมื่อใด สังเกตว่าคุณได้กำหนดอินเทอร์เฟซ 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();
});
}
}
ขั้นตอนที่ 2 ให้เริ่ม Listener ของฐานข้อมูลระหว่างการเริ่มต้นโปรแกรมเล่นใน 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 ไปที่เมนูบริบท แล้วเลือกแอป 1 แอปให้เป็นอุปกรณ์ผู้นำ คุณควรเห็นอุปกรณ์ที่ติดตามอยู่ เปลี่ยนแปลงไปเมื่อผู้นำอัปเดตอุปกรณ์
- จากนั้นเปลี่ยนอุปกรณ์ผู้นำ เล่นหรือหยุดเพลงชั่วคราว และสังเกตอุปกรณ์ผู้ติดตามที่อัปเดตตามความเหมาะสม
หากอุปกรณ์ของผู้ติดตามอัปเดตอย่างถูกต้อง แสดงว่าคุณสร้างตัวควบคุมข้ามอุปกรณ์สำเร็จแล้ว เหลืออีกเพียงขั้นตอนสำคัญ
7. อัปเดตกฎความปลอดภัย
นอกจากเราจะเขียนกฎความปลอดภัยที่ดีขึ้น อาจมีคนเขียนสถานะลงในอุปกรณ์ที่ตนไม่ได้เป็นเจ้าของได้ ดังนั้นก่อนที่จะดำเนินการเสร็จสิ้น โปรดอัปเดตกฎความปลอดภัยของฐานข้อมูลเรียลไทม์เพื่อให้แน่ใจว่ามีเพียงผู้ใช้ที่อ่านหรือเขียนลงในอุปกรณ์ได้เท่านั้นที่จะเป็นผู้ที่ลงชื่อเข้าใช้อุปกรณ์นั้น ในคอนโซล Firebase ให้ไปที่ Realtime Database แล้วไปที่แท็บกฎ วางกฎต่อไปนี้ที่อนุญาตให้ผู้ใช้ที่ลงชื่อเข้าใช้เท่านั้นที่จะอ่านและเขียนสถานะอุปกรณ์ของตนเองได้
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
}
}
8. ยินดีด้วย
ยินดีด้วย คุณสร้างรีโมตคอนโทรลข้ามอุปกรณ์โดยใช้ Flutter สำเร็จแล้ว
เครดิต
Better Together เพลงจาก Firebase
- เพลงของ Ryan Vernon
- เนื้อเพลงและปกอัลบั้มโดย Marissa Christy
- เสียงโดย JP Gomez
9. โบนัส
เพื่อเพิ่มความท้าทาย ให้ลองใช้ Flutter FutureBuilder
เพื่อเพิ่มอุปกรณ์ประเภทผู้มีโอกาสเป็นลูกค้าปัจจุบันลงใน UI แบบไม่พร้อมกัน หากคุณต้องการความช่วยเหลือ ชุดเครื่องมือนี้จะมีการใช้งานในโฟลเดอร์ที่มีสถานะเสร็จสิ้นแล้วของ Codelab