Inleiding
In de vorige Flutter App post is het raamwerk van de Best Practices App opgezet. Maar voor het Minimum Viable Product af is, dient de app iedere ochtend een notificatie te geven dat er een nieuwe practice klaar staat. In deze post wordt beschreven hoe je dat kunt doen.
Flutter Local Notifications Package
Gelukkig hoef je het notificatiedeel niet helemaal vanaf nul te bouwen. Er is namelijk een aantal packages die het meeste vuile werk al voor je opknappen. Voor deze app is ervoor gekozen om de flutter_local_notifications package te gebruiken. Deze package is gekozen omdat het de meeste likes heeft, een Flutter favourite is, en het redelijk actief onderhouden wordt (Het nadeel van deze package is dat het niet voor het web werkt, maar gelukkig wel voor Android en iOS (het is mij tot dusver nog niet gelukt om het voor het web werkend te krijgen, misschien een keer leuk om dat voor de community te maken?).
De Interface
Het is vaak niet verstandig om de code van een package rechtstreeks te gebruiken. Want wanneer je dan van package wilt veranderen, moet je overal waar je deze package gebruikt hebt de code wijzigen. Het is daarom verstandiger om je package te verpakken (wrappen) door een eigen klasse. Zelf noemde ik dit altijd een Wrapper, maar tijdens het schrijven van deze post heb ik gegoogled op dit concept en kwam ik te weten dat Martin Fowler dit een Gateway noemt. Dit is wat Martin Fowler over een Gateway zegt:
An object that encapsulates access to an external system or resource.
Een ander voordeel van een Gateway is dat je alleen de functionaliteiten van de package die je nodig hebt kan verpakken. Met de Gateway maak je de overbodige functionaliteiten onbereikbaar. Alleen wat heb je dan wel nodig? Dat kan je vastleggen in de interface van je Gateway.
In de interface bepaal je dus wat je nodig hebt voor je applicatie. In het geval van deze app zijn er eigenlijk maar twee dingen nodig (en dit kan natuurlijk nog veranderen wanneer er meer features bij de app komen).
- Je moet een nieuwe notificatie kunnen instellen.
- Je moet kunnen achterhalen of er al een notificatie is ingesteld.
In Dart gebruik je de term abstract
om een interface te maken. Wanneer je dan inherit
van die abstracte klasse word je door de compiler gewaarschuwd dat je de methodes in de abstracte klasse moet implementeren. Hieronder staat de code van de abstracte klasse INotificationsApi
.
abstract class INotificationsApi {
const INotificationsApi();
// Returns if a Notification of the API is pending
Future<bool> get isNotificationPending;
/// Set a new notification
Future<void> setNotification(
String message,
DateTime notificationDate,
);
}
Nadat de interface gemaakt is, is het tijd voor de implementaties.
De Implementaties
Hoewel de flutter_local_notifications package ook al per besturingssysteem (OS, Operating System, in dit geval iOS of Android) verschillende implementaties heeft, is het in dit geval toch handig om dat zelf ook te maken. Voor ieder type OS moet je namelijk zelf de initiële configuratie doorgeven. Dit kan je heel goed doen met een Factory.
De Factory maakt de juiste klasse op basis van het OS (zie code hieronder).
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:notification_api/notification_api.dart';
import 'android_local_notification_api.dart';
import 'ios_local_notification_api.dart';
abstract class LocalNotificationApi extends INotificationsApi {
factory LocalNotificationApi(String platform, String notificationTitle,
FlutterLocalNotificationsPlugin notificationsPlugin) {
switch (platform) {
case 'android':
return AndroidLocalNotificationApi(
notificationsPlugin, notificationTitle);
case 'iOS':
return IosLocalNotificationApi(notificationsPlugin, notificationTitle);
default:
throw UnsupportedError('No NotificationApi implemented for this platform.');
}
}
Future<void> configureLocalTimeZone();
}
Vervolgens zorg je voor beide implementaties. Je kunt in de implementaties allerlei methodes en attributen toevoegen, zolang je maar de abstract
klasse overschrijft en dus de methodes die gedefinieerd zijn in die klasse implementeert (zie code hieronder voor het Android voorbeeld).
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
import 'package:local_notification_api/src/local_notification_api.dart';
import 'package:notification_api/notification_api.dart' as api;
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
class AndroidLocalNotificationApi implements LocalNotificationApi {
final FlutterLocalNotificationsPlugin notificationsPlugin;
final String notificationsTitle;
final Future<String> localTimeZone;
AndroidLocalNotificationApi(this.notificationsPlugin, this.notificationsTitle)
: localTimeZone = FlutterNativeTimezone.getLocalTimezone() {
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon');
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
);
notificationsPlugin.initialize(initializationSettings,);
_requestPermission();
configureLocalTimeZone();
}
@override
Future<bool> get isNotificationPending async {
final pendingNotifications = await getPendingNotifications();
return pendingNotifications
.where((notification) => notification?.title == notificationsTitle)
.isNotEmpty;
}
Future<bool?> _requestPermission() async {
return notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
}
Future<List<api.PendingNotifications?>> getPendingNotifications() async {
final List<PendingNotificationRequest> pendingNotificationRequests = await notificationsPlugin.pendingNotificationRequests();
return pendingNotificationRequests.map(
(notification) => api.PendingNotifications(
id: notification.id,
title: notification.title,
body: notification.body,
)).toList();
}
@override
Future<void> setNotification(
String message,
DateTime notificationDate,
) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'daily_practices_channel', 'daily_practices_channel',
channelDescription:
'daily practices give a notification once a day',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker');
final timeTZ = tz.TZDateTime.from(notificationDate, tz.getLocation(await localTimeZone));
return await notificationsPlugin.zonedSchedule(
UniqueKey().hashCode,
notificationsTitle,
message,
timeTZ,
const NotificationDetails(
android: androidPlatformChannelSpecifics,
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
);
}
@override
configureLocalTimeZone() async {
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation(await localTimeZone));
}
}
Dat was het alweer voor deze post. Wil je meer informatie over de code? Maak dan een issue aan in de Github Repo. Hier is de code te downloaden tot dit punt. En in de volgende gaan we de hardcoded implementatie vervangen met Bloc.
Ontzettend bedankt voor het lezen :).
Mees