일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- REST API
- Redux
- userevent_tracker
- Flutter
- react
- identifierForVender
- Three-fiber
- swagger-typescript-api
- Babel standalone
- web track
- KakaoMap
- webrtc
- Raycasting
- uint8array
- node
- androidId
- uint16array
- jszip
- Game js
- Prism.js
- Excel
- typescript
- methodChannel
- code editor
- babel
- Three js
- RouteObserver
- FirebaseAnalytics
- Image Resize typescript
- Completer
- Today
- Total
Never give up
Flutter - FCM device to device, device to all devices 본문
알림 기능을 위해 FCM을 써야될 경우가 생기는데
공식문서가 생각보다 덜 친절하다보니 많이 질문을 올리기도 하고
필자도 구현하면서 조금 귀찮았던 부분이 있어서 이 부분에 대해 포스트 해보기로 했습니다
해당 예제에서는 FCM을 이해하는데 필요하다고 생각되는 부분만 짚고 넘어갈 예정이라
부분적으로 생략한 부분이 많아서 직접 구현하셔야되는게 조금 있는데
어려운 부분은 아니라고 생각돼서 넘어가겠습니다
(생략한 부분 : 회원가입과 로그인 및 기기 토큰값 firestore에 저장하는 작업 등등)
백그라운드 처리를 위한 메인부분
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
//이하 생략
}
포그라운드 및 UI처리부분
class _FCMTestMainState extends State<FCMTestMain> {
String _getMessage = 'N/A';
String _sendMessage = '';
StreamController<String> _controller = StreamController<String>();
FCMController _fcm = FCMController();
@override
void initState() {
super.initState();
fcm.getInitialMessage().then((value) {
if (value != null) {
_getMessage = value.toString();
}
});
/// Foreground 메시지 처리부분
FirebaseMessaging.onMessage.listen((message) {
RemoteNotification notification = message.notification!;
_getMessage =
'title : ${notification.title}, message : ${notification.body}';
_controller.add(_getMessage);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Message is arrived')));
print('Got a message whilst in the foreground!');
print('Message data: ${message.data}');
});
}
@override
void dispose() {
_controller.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
var model = Provider.of<ProfileData>(context);
FocusScopeNode scope = FocusScope.of(context);
return Scaffold(
appBar: AppBar(
title: Text('FCM test page'),
actions: [
IconButton(
icon: Icon(Icons.power_settings_new),
onPressed: () => auth.signOut().whenComplete(() {
Navigator.pop(context);
}))
],
),
body: SafeArea(
child: SingleChildScrollView(
child: InkWell(
focusColor: Colors.white,
onTap: () {
if (scope.hasFocus) {
scope.unfocus();
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StreamBuilder<String>(
stream: _controller.stream,
initialData: _getMessage,
builder: (context, snapshot) {
return ListTile(
title: Text('Message status : ${snapshot.data}'));
}),
ListView.builder(
shrinkWrap: true,
itemCount: model.list.length,
itemBuilder: (_, index) {
var item = model.list[index];
bool isMyProfile = (auth.currentUser!.email == item.id);
return Card(
margin: EdgeInsets.all(8),
elevation: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text('Id : ${item.id}'),
trailing: isMyProfile
? Switch(
value: item.isSubscribed,
onChanged: (value) async {
StatusData statusData =
await model.toggleSubscribe(item.id);
if (statusData.isFailed) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(
statusData.errorStack)));
}
})
: null,
),
ListTile(
title: Text('DeviceToken : ${item.deviceToken}',
maxLines: 2,
overflow: TextOverflow.ellipsis)),
isMyProfile
? Container()
: SizedBox(
height: 50,
child: TextField(
textAlignVertical: TextAlignVertical.center,
maxLines: 1,
decoration: InputDecoration(
border: OutlineInputBorder(),
suffix: IconButton(
icon: CircleAvatar(
child: Icon(Icons.send)),
onPressed: () async {
print(item.id);
if (_sendMessage.isNotEmpty) {
StatusData statusData =
await _fcm.sendMessage(
title: 'FCM test',
message: _sendMessage,
userToken:
item.deviceToken);
if (statusData.isFailed) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(statusData
.errorStack)));
}
}
},
),
),
onChanged: (value) {
_sendMessage = value;
},
),
)
],
),
);
},
)
],
),
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
String notiAll = '';
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Send message to all users'),
content: SizedBox(
height: 50,
child: TextField(
onChanged: (value) {
notiAll = value;
},
)),
actions: [
TextButton(
child: Text('Confirm'),
onPressed: () async {
if (notiAll.isNotEmpty) {
StatusData statusData = await _fcm.sendToAllUsers(
title: 'Notice', message: notiAll);
if (statusData.isFailed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(statusData.errorStack)));
}
Navigator.pop(context);
}
}),
TextButton(
child: Text('Cancel'),
onPressed: () => Navigator.pop(context)),
],
),
);
},
),
);
}
}
FCM controller class
class FCMController {
static String _serverKey = 'yourKey';//firebase 페이지에서 확인할 수 있습니다
Future<void> iosPermission() async {
NotificationSettings settings = await fcm.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: false,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted permission');
} else if (settings.authorizationStatus ==
AuthorizationStatus.provisional) {
print('User granted provisional permission');
} else {
print('User declined or has not accepted permission');
}
}
Future<StatusData> sendMessage(
{required String userToken,
required String title,
required String message}) async {
StatusData statusData = StatusData();
late http.Response response;
try {
response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json',
'Authorization': 'key=$_serverKey'
},
body: jsonEncode({
'notification': {'title': title, 'body': message, 'sound': 'true'},
'priority': 'high',
'ttl': '60s',
'data': {
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done',
},
'to': userToken
}));
} catch (e) {
statusData.hasError();
statusData.errorStack = e.toString();
}
print('statusCode : ${response.statusCode}');
return statusData;
}
Future<StatusData> sendToAllUsers(
{required String title, required String message}) async {
StatusData statusData = StatusData();
late http.Response response;
try {
response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json',
'Authorization': 'key=$_serverKey'
},
body: jsonEncode({
'notification': {'title': title, 'body': message, 'sound': 'true'},
'priority': 'high',
'ttl': '60s',
'data': {
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done'
},
'to': '/topics/test'
}));
} catch (e) {
statusData.hasError();
statusData.errorStack = e.toString();
}
print('statusCode : ${response.statusCode}');
return statusData;
}
}
profile data class
class ProfileData extends ChangeNotifier {
final CollectionReference _profile = cloud.collection('profile');
List<ProfileDataModel> list = [];
Future<void> readData() async {
list.clear();
QuerySnapshot snapshot = await _profile.get();
snapshot.docs.forEach((element) {
list.add(ProfileDataModel.fromJson(element.data()));
});
}
Future<StatusData> saveProfile() async {
StatusData statusData = StatusData();
ProfileDataModel model = ProfileDataModel();
User user = auth.currentUser!;
String email = user.email!;
model.id = email;
model.deviceToken = (await fcm.getToken())!;
try {
await _profile.doc(email).set(model.toJson());
} catch (e) {
statusData.hasError();
statusData.errorStack = e.toString();
}
if (!statusData.isFailed) {
list.add(model);
}
return statusData;
}
Future<String> appleToken() async {
String appleDeviceToken = '';
try {
appleDeviceToken = (await fcm.getAPNSToken())!;
} catch (e) {
print(e.toString());
}
return appleDeviceToken;
}
Future<StatusData> toggleSubscribe(String id) async {
StatusData statusData = StatusData();
ProfileDataModel profileData = list.singleWhere((element) => element.id == id);
try {
if (profileData.isSubscribed) {
await fcm.unsubscribeFromTopic('test');
profileData.isSubscribed = false;
} else {
await fcm.subscribeToTopic('test');
profileData.isSubscribed = true;
}
await _profile.doc(profileData.id).set(profileData.toJson());
} catch (e) {
statusData.hasError();
statusData.errorStack = e.toString();
}
if (!statusData.isFailed) {
notifyListeners();
}
return statusData;
}
}
마지막으로 필자의 간단한 에러 처리 클래스
class StatusData {
bool isFailed;
String errorStack;
StatusData({this.isFailed = false, this.errorStack = ''});
void hasError() {
isFailed = true;
}
}
생략해도 UI부분 때문에 소스코드가 좀 길어졌네요..
백그라운드 부분은 onBackgroundMessage, 포그라운드 부분은 onMessage.listen에서 처리를 해주고
필자의 경우 UI부분에서 streamBuilder와 snackbar를 이용해서 메시지를 출력해주는 정도만 하고 있습니다
FCM을 개인과 개인이 주고받을 때 그리고 개인과 다수가 받을 때 따로따로 보면 다음과 같습니다
- 개인과 개인이 메시지를 보낼 때
1. 기기 토큰값을 기기로부터 가져온다
2. 기기 토큰값을 서버에 저장한다
3. 메시지를 보낼 유저의 토큰값을 가져온다
4. 메시지를 보낸다
5. 앱이 꺼져있으면 백그라운드 켜져있으면 포그라운드에서 처리한다
- 개인이 단체한테 메시지를 보낼 때
1. 유저가 해당 topic를 subscribe한다
2. 메시지를 보낸다
3. subscribe한 유저들은 메시지를 받는다
4. 위 5번과 동일
이 부분만 이해하면 나머지는 열심히 삽질(?)하다보면 되더군요
그리고 주의해야되는 부분은 app이 terminated 되었을 때는 별도의 처리를 해줘야되는거 같고
device token값이 항상 동일하지는 않았던거 같아서 공식문서를 확인해 보니 다음과 같은 부분이 있었습니다
// Any time the token refreshes, store this in the database too.
FirebaseMessaging.instance.onTokenRefresh.listen(saveTokenToDatabase);
추가로 메시지를 보낼때 같이 보낼 수 있는 데이터들(?)은 공식문서에서 확인을 하시면 될거 같습니다
(링크 : firebase.google.com/docs/reference/fcm/rest/v1/projects.messages)
'Flutter' 카테고리의 다른 글
Flutter - WidgetsBindingObserver (0) | 2021.05.22 |
---|---|
Flutter 2.0 - typedef callback with ScaffoldMessenger Widget, global key (0) | 2021.05.08 |
Flutter - Loop PageView (0) | 2021.04.17 |
Flutter 2.0 - Hive fixed length list problem and solution (2) | 2021.03.31 |
Flutter - ListView inside ListView with shrinkwrap (3) | 2021.03.28 |