はじめに
ミーアの新機能や店頭販売などのお知らせに関して、WordPressで作成しているミーアのHPのお知らせカテゴリに、お知らせを掲載して、新規にお知らせが追加されたら、アプリにプッシュ通知するとともに、そのプッシュ通知をクリックしたらお知らせ一覧の画面に遷移するようにしたい。
つまり、お知らせに関しては、HPに掲載で一元管理としたい。
今回は、HPに掲載するお知らせ一覧のWebビュー表示と、新規お知らせのアプリプッシュ通知の実装を試みる。
お知らせ一覧のWebビュー表示
今回は、お知らせの動線を画面下部のフッターではなく、設定画面の中の最上部に設置しようと思う。
設定画面にリストアイテムとして「お知らせ」を追加し、それをタップするとNoticeScreen
を表示する。
/lib/screens/home/settings_tab.dart
import 'package:clocky_app/screens/home/notice_screen.dart';
// 省略
class SettingsTab extends ConsumerStatefulWidget {
const SettingsTab({super.key});
@override
_SettingsTabState createState() => _SettingsTabState();
}
class _SettingsTabState extends ConsumerState<SettingsTab> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("設定"),
automaticallyImplyLeading: false,
),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: <Widget>[
buildSection(['お知らせ']), // お知らせセクションの追加
// 他のセクションは省略
],
),
);
}
Widget buildSection(List<String> items) {
return Container(
margin: const EdgeInsets.only(bottom: 24.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5.0),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1.0,
blurRadius: 5.0,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
items.map((item) => buildItem(item, items.last == item)).toList(),
),
);
}
Widget buildItem(String item, bool isLast) {
return Container(
decoration: BoxDecoration(
border: isLast
? null
: const Border(
bottom: BorderSide(
color: Colors.grey,
width: 0.5,
),
),
),
child: Column(
children: [
ListTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
if (item == 'お知らせ') {
showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: false,
builder: (_) => const FractionallySizedBox(
heightFactor: 0.85,
child: NoticeScreen(),
),
);
}
},
),
],
),
);
}
}
notice_screen.dartではWebViewController
を初期化し、指定されたURL(https://mia-cat.com/category/notice/
)をロードする。
/lib/screens/home/notice_screen.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class NoticeScreen extends StatefulWidget {
const NoticeScreen({super.key});
@override
_NoticeScreenState createState() => _NoticeScreenState();
}
class _NoticeScreenState extends State<NoticeScreen> {
late WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..loadRequest(Uri.parse('https://mia-cat.com/category/notice/'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('お知らせ'),
leading: Container(),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
body: WebViewWidget(controller: _controller),
);
}
}
無事このように、設定画面の最上部に『お知らせ』が追加され、クリックすると、WebビューでHPのお知らせカテゴリに掲載された記事一覧が表示されるようになった。
FCM通知とタップ時の遷移制御
こちらに関しては、最初自動化を試みた。つまり、WordPressのお知らせカテゴリに新着投稿記事が掲載されたら、自動的にアプリにプッシュ通知が届く仕様。
ただ、実装がいくらか複雑だったのと、そんなに頻繁にお知らせするのは現時点で考えにくいので、一旦はFirebaseコンソールから通知を手動送信する方法に切り替えることにした。
以前、こちらの記事で、FCM通知をFirebaseコンソールから手動でテスト送信するのは記載した。
この時は、通知をタップしたらホーム画面に移動するだけだったので、タップ時の画面遷移の制御は必要なかったが、今回は設定画面に画面遷移するようにしたいので、アプリのコードを少し優勢する。
main.dart
を以下のように修正する。
トピックへのサブスクライブ
すべてのユーザーがall_users
というトピックにサブスクライブする。これにより、all_users
トピックに対して送信された通知は、すべてのサブスクライブしているユーザーに届く。
FirebaseMessaging.instance.subscribeToTopic('all_users');
通知をタップした時の画面遷移
_handleMessage関数:受信したメッセージのデータにnotice
というキーが含まれているかを確認し、notice
キーの値がdetails
である場合、SettingTab
画面に遷移するようにする。
void _handleMessage(RemoteMessage message) {
if (message.data.containsKey('notice')) {
final screen = message.data['notice'];
if (screen == 'details') {
navigatorKey.currentState?.push(MaterialPageRoute(builder: (_) => const SettingTab()));
}
}
}
main.dart 全体
import 'package:clocky_app/app_switcher.dart';
import 'package:clocky_app/firebase_options.dart';
import 'package:clocky_app/screens/home/settings_tab.dart';
import 'package:clocky_app/styles/colors.dart';
import 'package:clocky_app/styles/text_styles.dart';
import 'package:clocky_app/widgets/show_custom_message_bar.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:overlay_support/overlay_support.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// ヘルスコネクトが利用できるようになった後に有効化する
// await initializeBackgroundService();
runApp(const ProviderScope(child: ClockyApp()));
}
class ClockyApp extends ConsumerStatefulWidget {
const ClockyApp({super.key});
@override
_ClockyAppState createState() => _ClockyAppState();
}
class _ClockyAppState extends ConsumerState<ClockyApp> {
@override
void initState() {
super.initState();
initializeApp();
setupFirebaseMessaging();
}
Future<void> initializeApp() async {
// フォアグラウンドでのPush通知
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) {
showCustomMessageBar(
context: navigatorKey.currentContext!,
title: message.notification!.title ?? "通知",
message: message.notification!.body ?? "新しい通知",
onClose: () {
debugPrint("Notification closed");
},
onTapped: () {
debugPrint("Notification tapped");
_handleMessage(message);
},
duration: Duration.zero,
);
}
});
}
void setupFirebaseMessaging() {
FirebaseMessaging.instance.subscribeToTopic('all_users');
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_handleMessage(message);
});
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage? message) {
if (message != null) {
_handleMessage(message);
}
});
}
void _handleMessage(RemoteMessage message) {
if (message.data.containsKey('notice')) {
final screen = message.data['notice'];
if (screen == 'details') {
navigatorKey.currentState
?.push(MaterialPageRoute(builder: (_) => const SettingsTab()));
}
}
}
@override
Widget build(BuildContext context) {
return OverlaySupport.global(
child: MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: navigatorKey,
title: 'Clocky App',
theme: ThemeData(
colorScheme: const ColorScheme.light(
primary: AppColors.primaryColor,
secondary: AppColors.primaryColorDark,
background: AppColors.backgroundColor,
onBackground: AppColors.textColor,
),
textTheme: const TextTheme(
displayLarge: TextStyles.titleStyle,
bodyLarge: TextStyles.bodyStyle,
),
),
routes: {
'/': (context) => const AppSwitcher(),
},
),
);
}
}
Firebaseコンソール画面での設定
Firebaseコンソール画面の方で、下記を入力することで、アプリ側でこのFCM通知が認識され、通知をタップしたときに設定画面に遷移できるようになる。
- トピック:all_users
- カスタムデータ)キー:notice, 値:details
動作確認
最近実装した、ミュート機能の記事を、カテゴリー「お知らせ」を選択して公開する。
公開後、Firebaseコンソールを開いて、下記のように通知タイトル・テキストを入力する。
そして、ターゲットで、トピック→all_usersを入力する。
今すぐ送信を選択し、その他のオプションのカスタムデータの欄に
- キー:notice
- 値:detail
を入力して、「確認」をクリック
再確認の ポップアップが表示されるので、公開をクリックする
フォアグラウンド
バックグラウンド
通知をタップした時
フォアグラウンドでもバックグラウンドでも無事通知が表示され、タップすると設定画面に遷移し、お知らせをクリックするとお知らせ一覧が表示されることを確認した。
実際の運用の際は、最初に作成したFCM通知を複製することで、通知条件(トピック・カスタムデータのキーや値)も踏襲されるので、複製して、メッセージのタイトルのみ変更して配信というのが良さそう。
手動ではあるが、意外と手間はかからなそうなので、当面はこの運用を試してみる。