A Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire.
Android |
---|
ā |
After importing this plugin to your project as usual, add the following to your AndroidManifest.xml
within the <manifest></manifest>
tags:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- For apps with targetSDK=31 (Android 12) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
Next, within the <application></application>
tags, add:
<service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Then in Dart code add:
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
// Be sure to annotate your callback function to avoid issues in release mode on Flutter >= 3.3.0
@pragma('vm:entry-point')
static void printHello() {
final DateTime now = DateTime.now();
final int isolateId = Isolate.current.hashCode;
print("[$now] Hello, world! isolate=${isolateId} function='$printHello'");
}
main() async {
// Be sure to add this line if initialize() call happens before runApp()
WidgetsFlutterBinding.ensureInitialized();
await AndroidAlarmManager.initialize();
runApp(...);
final int helloAlarmID = 0;
await AndroidAlarmManager.periodic(const Duration(minutes: 1), helloAlarmID, printHello);
}
printHello
will then run (roughly) every minute, even if the main app ends. However, printHello
will not run in the same isolate as the main application. Unlike threads, isolates do not share memory and communication between isolates must be done via message passing (see more documentation on isolates here).
If alarm callbacks will need access to other Flutter plugins, including the alarm manager plugin itself, it may be necessary to inform the background service how to initialize plugins depending on which Flutter Android embedding the application is using.
From the Android AlarmManager documentation:
Registered alarms are retained while the device is asleep (and can optionally wake the device up if they go off during that time), but will be cleared if it is turned off and rebooted.
https://developer.android.com/reference/android/app/AlarmManager
The Android OS will not fire alarms for apps that have been force stopped.
StackOverflow response: https://stackoverflow.com/questions/11241794/alarm-set-in-app-with-alarmmanager-got-removed-when-app-force-stop
Likely the device is running some battery optimization software that is preventing the alarm from firing. Check out https://dontkillmyapp.com/ to find out about more about optimizations done by different vendors.
Run normally with flutter test
from the root of the project.
The Espresso test runs the same sample code provided in example/lib/main.dart
but is run using the Flutter Espresso plugin.
Modifying the main.dart
will cause this test to fail.
This test will call into the example/lib/main_espresso.dart
file which will enable Flutter Driver and then calls into the main.dart
.
See https://pub.dev/packages/espresso for more info on why.
To run the test, run from the example/android
folder:
./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../lib/main_espresso.dart
To run the Flutter Driver tests, cd into example
and run:
flutter driver test_driver/android_alarm_manager_plus_e2e.dart
Run this command:
With Flutter:
$ flutter pub add android_alarm_manager_plus
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
android_alarm_manager_plus: ^3.0.2
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:developer' as developer;
import 'dart:isolate';
import 'dart:math';
import 'dart:ui';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
/// The [SharedPreferences] key to access the alarm fire count.
const String countKey = 'count';
/// The name associated with the UI isolate's [SendPort].
const String isolateName = 'isolate';
/// A port used to communicate from a background isolate to the UI isolate.
ReceivePort port = ReceivePort();
/// Global [SharedPreferences] object.
SharedPreferences? prefs;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Register the UI isolate's SendPort to allow for communication from the
// background isolate.
IsolateNameServer.registerPortWithName(
port.sendPort,
isolateName,
);
prefs = await SharedPreferences.getInstance();
if (!prefs!.containsKey(countKey)) {
await prefs!.setInt(countKey, 0);
}
runApp(const AlarmManagerExampleApp());
}
/// Example app for Espresso plugin.
class AlarmManagerExampleApp extends StatelessWidget {
const AlarmManagerExampleApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0x9f4376f8),
),
home: const _AlarmHomePage(),
);
}
}
class _AlarmHomePage extends StatefulWidget {
const _AlarmHomePage({Key? key}) : super(key: key);
@override
_AlarmHomePageState createState() => _AlarmHomePageState();
}
class _AlarmHomePageState extends State<_AlarmHomePage> {
int _counter = 0;
@override
void initState() {
super.initState();
AndroidAlarmManager.initialize();
// Register for events from the background isolate. These messages will
// always coincide with an alarm firing.
port.listen((_) async => await _incrementCounter());
}
Future<void> _incrementCounter() async {
developer.log('Increment counter!');
// Ensure we've loaded the updated count from the background isolate.
await prefs?.reload();
setState(() {
_counter++;
});
}
// The background
static SendPort? uiSendPort;
// The callback for our alarm
@pragma('vm:entry-point')
static Future<void> callback() async {
developer.log('Alarm fired!');
// Get the previous cached count and increment it.
final prefs = await SharedPreferences.getInstance();
final currentCount = prefs.getInt(countKey) ?? 0;
await prefs.setInt(countKey, currentCount + 1);
// This will be null if we're running in the background.
uiSendPort ??= IsolateNameServer.lookupPortByName(isolateName);
uiSendPort?.send(null);
}
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.headlineMedium;
return Scaffold(
appBar: AppBar(
title: const Text('Android alarm manager plus example'),
elevation: 4,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Alarm fired $_counter times',
style: textStyle,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Total alarms fired: ',
style: textStyle,
),
Text(
prefs?.getInt(countKey).toString() ?? '',
key: const ValueKey('BackgroundCountText'),
style: textStyle,
),
],
),
const SizedBox(height: 24),
ElevatedButton(
key: const ValueKey('RegisterOneShotAlarm'),
onPressed: () async {
await AndroidAlarmManager.oneShot(
const Duration(seconds: 5),
// Ensure we have a unique alarm ID.
Random().nextInt(pow(2, 31) as int),
callback,
exact: true,
wakeup: true,
);
},
child: const Text(
'Schedule OneShot Alarm',
),
),
],
),
),
);
}
}
Download details:
Author: fluttercommunity.dev
Source: https://github.com/fluttercommunity/plus_plugins/tree/main/packages/android_alarm_manager_plus