My app uses flutter_local_notification plugin.
I need to make a transition on particular screen when my app is terminated. Everything works fine when the app isn't closed. This is what I've tried.
I scoured the plugins' docs but didn't find the answer. Is it possible to do so without push notifications?
Maybe i should use native channels to run the actions?
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
import 'package:like_cucumber/core/like_cucumber.dart';
import 'package:like_cucumber/data/mappers/task_notification_payload_mapper.dart';
import 'package:like_cucumber/presentation/tab_bar/tab_bar/tab_bar.dart';
import 'package:like_cucumber/presentation/tab_bar/tasks_tab/task_info.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
@pragma('vm:entry-point')
void onDidReceiveBackgroundNotification(NotificationResponse response) async {
final payload = response.payload;
if (payload == null) return;
final task = TaskNotificationPayloadMapper.fromPayload(payload);
navigatorKey.currentState?.push(TabBarPage.getRoute(initialTabIndex: 1));
navigatorKey.currentState?.push(TaskInfoPage.getRoute(
task: task,
));
}
class LocalNotificationsService {
factory LocalNotificationsService() => _instance;
LocalNotificationsService._();
static final _instance = LocalNotificationsService._();
final _plugin = FlutterLocalNotificationsPlugin();
final _messaging = FirebaseMessaging.instance;
Future<void> init() async {
await _requestPermission();
await _initCurrentTimezone();
await _initPlugin();
}
Future<void> _requestPermission() => _messaging.requestPermission();
Future<void> _initCurrentTimezone() async {
tz.initializeTimeZones();
final localName = await FlutterNativeTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(localName));
}
Future<void> _initPlugin() async {
const DarwinInitializationSettings iosSettings =
DarwinInitializationSettings();
const initSettings = InitializationSettings(iOS: iosSettings);
await _plugin.initialize(
initSettings,
onDidReceiveBackgroundNotificationResponse:
onDidReceiveBackgroundNotification,
onDidReceiveNotificationResponse: (response) {
_notificationListenerController.add(response);
},
);
}
late Stream<NotificationResponse> notificationListener =
_notificationListenerController.stream.asBroadcastStream();
final _notificationListenerController =
StreamController<NotificationResponse>();
NotificationDetails get _notificationDetails =>
const NotificationDetails(iOS: DarwinNotificationDetails());
Future<void> scheduleNotification({
required int id,
required String title,
required String message,
required DateTime dateTime,
String? payload,
}) async {
await _plugin.zonedSchedule(
id,
title,
message,
_convertToTZDate(dateTime),
_notificationDetails,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.dateAndTime,
payload: payload,
);
}
tz.TZDateTime _convertToTZDate(DateTime date) => tz.TZDateTime(
tz.local,
date.year,
date.month,
date.day,
date.hour,
date.minute,
date.second,
);
Future<void> cancel(int id) => _plugin.cancel(id);
Future<void> cancelAll() => _plugin.cancelAll();
}
My info.plist has permissions (however I don't think it's required):
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
I appreciate any help.
Ok, finally I double check the docs and finished up with this:
main.dart
String? payload;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// some initialization code
await _checkDynamicLinks();
runApp(LikeCucumber(payload: payload));
}
Future<void> _checkDynamicLinks() async {
final plugin = FlutterLocalNotificationsPlugin();
await LocalNotificationsService().init(plugin);
await _getNotificationAppLaunchDetails(plugin);
}
Future<void> _getNotificationAppLaunchDetails(
FlutterLocalNotificationsPlugin plugin,
) async {
final details = await plugin.getNotificationAppLaunchDetails();
if (details?.notificationResponse != null) {
payload = details!.notificationResponse?.payload;
}
}
like_cucumber.dart
final navigatorKey = GlobalKey<NavigatorState>();
class LikeCucumber extends StatelessWidget {
const LikeCucumber({required this.payload, Key? key}) : super(key: key);
final String? payload;
@override
Widget build(BuildContext context) {
return MultiProvider(
child: MaterialApp(
// ...
navigatorKey: navigatorKey,
onGenerateRoute: (settings) =>
AppRouter.onGenerateRoute(settings, payload),
initialRoute: LaunchPage.routeName,
),
);
}
}
app_router.dart
class AppRouter {
AppRouter._();
// когда переходит по диплинку, то не переносит данные
// нужно не делать переход по экранам
static Route? onGenerateRoute(RouteSettings settings, [String? payload]) {
switch (settings.name) {
case LaunchPage.routeName:
return LaunchPage.getRoute(sl(), payload);
// ...
}
return null;
}
// ...
}
launch_page.dart
class LaunchPage extends StatefulWidget {
const LaunchPage._(
{required this.userAuthStateRepo,
required this.notificationDetailPayload});
static const String routeName = '/';
final UserAuthStateRepo userAuthStateRepo;
final String? notificationDetailPayload;
static getRoute(
UserAuthStateRepo userAuthStateRepo, String? notificationDetailPayload) {
return MaterialPageRoute(
settings: const RouteSettings(name: routeName),
builder: (_) => LaunchPage._(
userAuthStateRepo: userAuthStateRepo,
notificationDetailPayload: notificationDetailPayload,
),
);
}
@override
State<LaunchPage> createState() => _LaunchPageState();
}
class _LaunchPageState extends State<LaunchPage> {
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async => await
_goTo());
super.initState();
}
Future<void> _goTo() async {
// ...
_goToTabBar();
}
void _goToTabBar() {
final task = _taskFromDeeplink;
(task == null) ? _goToInitialTabBarPage() : _goToTaskDetail(task);
}
Task? get _taskFromDeeplink {
final payload = widget.notificationDetailPayload;
if (payload == null) return null;
final task = TaskNotificationPayloadMapper.fromPayload(payload);
return task;
}
void _goToTaskDetail(Task task) {
navigatorKey.currentState?.pushAndRemoveUntil(
TabBarPage.getRoute(initialTabIndex: 1),
(_) => false,
);
navigatorKey.currentState?.push(TaskInfoPage.getRoute(task: task));
}
@override
Widget build(BuildContext context) {
// ...
}
}
tasks_info_page.dart
class TaskInfoPage extends StatelessWidget {
const TaskInfoPage._({required Task task});
static getRoute({
required Task task,
}) {
return MaterialPageRoute(
settings: const RouteSettings(name: routeName),
builder: (_) => ChangeNotifierProvider(
create: (context) => TaskInfoPageModel(
task: task,
tasksRepo: sl(),
tasksCompleter: sl(),
),
child: TaskInfoPage._(task: task),
),
);
}
// ...
The key points of my issue and the solution:
For sure this is a workaround, there is no good architecture solution here. However this works. If you have any better approaches how to organize this I will be happy to see them.