Introduction
Unlock the potential of Flutter’s Background Fetch for smooth data updates in your iOS and Android apps! ???? This easy-to-follow article will walk you through the setup process and make sure your Flutter app is always up to date, even while it is running in the background. Gain mastery over Background Fetch features to improve user experience while optimising performance on both iOS and Android devices. Watch now to maintain the dynamic and responsiveness of your app and improve your Flutter programming abilities. With this thorough Background Fetch lesson, you can stay ahead in the Flutter game! ???????? #FlutterDevelopment #BackgroundFetch #MobileAppDevelopment
Defaults to `15` minutes. Note: Background-fetch events will never occur at a frequency higher than every 15 minutes. Apple uses a secret algorithm to adjust the frequency of fetch events, presumably based upon usage patterns of the app. Fetch events can occur less often than your configured `minimumFetchInterval`.
Installing the plugin
pubspec.yaml
:
dependencies:
background_fetch: '^1.1.3'
Or latest from Git:
dependencies:
background_fetch:
git:
url: https://github.com/transistorsoft/flutter_background_fetch
Android Setup
AndroidManifest
Flutter seems to have a problem with 3rd-party Android libraries which merge their own AndroidManifest.xml
into the application, particularly the android:label
attribute.
:open_file_folder: android/app/src/main/AndroidManifest.xml
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.example.helloworld">
<application
+ tools:replace="android:label"
android:name=".Application"
android:label="flutter_background_geolocation_example"
android:icon="@mipmap/ic_launcher">
</manifest>
⚠️ Failure to perform the step above will result in a build error
Execution failed for task ':app:processDebugManifest'.
> Manifest merger failed : Attribute application@label value=(hello_world) from AndroidManifest.xml:17:9-36
is also present at [tslocationmanager-2.13.3.aar] AndroidManifest.xml:24:18-50 value=(@string/app_name).
Suggestion: add 'tools:replace="android:label"' to <application> element at AndroidManifest.xml:15:5-38:19 to override.
android/build.gradle
As an app grows in complexity and imports a variety of 3rd-party modules, it helps to provide some key “Global Gradle Configuration Properties” which all modules can align their requested dependency versions to. background_fetch
is aware of these variables and will align itself to them when detected.
:open_file_folder: android/build.gradle
:
buildscript {
ext.kotlin_version = '1.3.72' // or latest
+ ext {
+ compileSdkVersion = 31 // or latest
+ targetSdkVersion = 31 // or latest
+ appCompatVersion = "1.4.2" // or latest
+ }
}
allprojects {
repositories {
google()
mavenCentral()
+ maven {
+ // [required] background_fetch
+ url "${project(':background_fetch').projectDir}/libs"
+ }
}
}
android/app/build.gradle
In addition, you should take advantage of the Global Configuration Properties yourself, replacing hard-coded values in your android/app/build.gradle
with references to these variables:
:open_file_folder: android/app/build.gradle
:
android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
.
.
.
defaultConfig {
.
.
.
+ targetSdkVersion rootProject.ext.targetSdkVersion
}
}
Precise event-scheduling with forceAlarmManager: true
:
Only If you wish to use precise scheduling of events with forceAlarmManager: true
, Android 14 (SDK 34), has restricted usage of “AlarmManager
exact alarms”. To continue using precise timing of events with Android 14, you can manually add this permission to your AndroidManifest
. Otherwise, the plugin will gracefully fall-back to “in-exact AlarmManager
scheduling”:
:open_file_folder: In your AndroidManifest
, add the following permission (exactly as-shown):
<manifest>
<uses-permission android:minSdkVersion="34" android:name="android.permission.USE_EXACT_ALARM" />
.
.
.
</manifest>
:warning: It has been announced that Google Play Store has plans to impose greater scrutiny over usage of this permission (which is why the plugin does not automatically add it).
Handler Mechanism enableless : true
Application.java
package com.example.flutterbackground;
import com.transistorsoft.flutter.backgroundfetch.BackgroundFetchPlugin;
import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class Application extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
BackgroundFetchPlugin.setPluginRegistrant(this);
}
@Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}
JavaiOS Setup
Configure Background Capabilities
- Select the root of your project. Select Capabilities tab. Enable Background Modes and enable the following mode:
- [x] Background fetch
- [x] Background processing (Only if you intend to use
BackgroundFetch.scheduleTask
)
Configure Info.plist
- Open your
Info.plist
and add the key “Permitted background task scheduler identifiers”
- Add the required identifier
com.transistorsoft.fetch
.
- If you intend to execute your own custom tasks via
BackgroundFetch.scheduleTask
, you must add those custom identifiers as well. For example, if you intend to execute a customtaskId: 'com.transistorsoft.customtask'
, you must add the identifiercom.transistorsoft.customtask
to your “Permitted background task scheduler identifiers”, as well.
:warning: Your custom task identifiers MUST be prefixed with com.transistorsoft.
.
BackgroundFetch.scheduleTask(TaskConfig(
taskId: 'com.transistorsoft.customtask',
delay: 60 * 60 * 1000 // In one hour (milliseconds)
));
DartSteps
init Platform State
Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
// Configure BackgroundFetch.
int status = await BackgroundFetch.configure(BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.NONE
), (String taskId) async { // <-- Event handler
// This is the fetch-event callback.
print("[BackgroundFetch] Event received $taskId");
setState(() {
_events.insert(0, new DateTime.now());
});
// IMPORTANT: You must signal completion of your task or the OS can punish your app
// for taking too long in the background.
BackgroundFetch.finish(taskId);
}, (String taskId) async { // <-- Task timeout handler.
// This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId)
print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId");
BackgroundFetch.finish(taskId);
});
print('[BackgroundFetch] configure success: $status');
setState(() {
_status = status;
});
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
}
DartbackgroundFetchHeadlessTask
[Android-only] This “Headless Task” is run when the Android app is terminated with `enableHeadless: true`
Be sure to annotate your callback function to avoid issues in release mode on Flutter >= 3.3.0
@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessTask task) async {
String taskId = task.taskId;
bool isTimeout = task.timeout;
if (isTimeout) {
// This task has exceeded its allowed running-time.
// You must stop what you're doing and immediately .finish(taskId)
print("[BackgroundFetch] Headless task timed-out: $taskId");
BackgroundFetch.finish(taskId);
return;
}
print('[BackgroundFetch] Headless event received.');
// Do your work here...
BackgroundFetch.finish(taskId);
}
DartStart and Stop Service
BackgroundFetch.start().then((int status) {
print('[BackgroundFetch] start success: $status');
}).catchError((e) {
print('[BackgroundFetch] start FAILURE: $e');
});
BackgroundFetch.stop().then((int status) {
print('[BackgroundFetch] stop success: $status');
});
DartFull Code
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:background_fetch/background_fetch.dart';
// [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true`
// Be sure to annotate your callback function to avoid issues in release mode on Flutter >= 3.3.0
@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessTask task) async {
String taskId = task.taskId;
bool isTimeout = task.timeout;
if (isTimeout) {
// This task has exceeded its allowed running-time.
// You must stop what you're doing and immediately .finish(taskId)
print("[BackgroundFetch] Headless task timed-out: $taskId");
BackgroundFetch.finish(taskId);
return;
}
print('[BackgroundFetch] Headless event received.');
// Do your work here...
BackgroundFetch.finish(taskId);
}
void main() {
// Enable integration testing with the Flutter Driver extension.
// See https://flutter.io/testing/ for more info.
runApp(new MyApp());
// Register to receive BackgroundFetch events after app is terminated.
// Requires {stopOnTerminate: false, enableHeadless: true}
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _enabled = true;
int _status = 0;
List<DateTime> _events = [];
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
// Configure BackgroundFetch.
int status = await BackgroundFetch.configure(BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.NONE
), (String taskId) async { // <-- Event handler
// This is the fetch-event callback.
print("[BackgroundFetch] Event received $taskId");
setState(() {
_events.insert(0, new DateTime.now());
});
// IMPORTANT: You must signal completion of your task or the OS can punish your app
// for taking too long in the background.
BackgroundFetch.finish(taskId);
}, (String taskId) async { // <-- Task timeout handler.
// This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId)
print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId");
BackgroundFetch.finish(taskId);
});
print('[BackgroundFetch] configure success: $status');
setState(() {
_status = status;
});
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
}
void _onClickEnable(enabled) {
setState(() {
_enabled = enabled;
});
if (enabled) {
BackgroundFetch.start().then((int status) {
print('[BackgroundFetch] start success: $status');
}).catchError((e) {
print('[BackgroundFetch] start FAILURE: $e');
});
} else {
BackgroundFetch.stop().then((int status) {
print('[BackgroundFetch] stop success: $status');
});
}
}
void _onClickStatus() async {
int status = await BackgroundFetch.status;
print('[BackgroundFetch] status: $status');
setState(() {
_status = status;
});
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
debugShowCheckedModeBanner: false,
home: new Scaffold(
backgroundColor: Colors.white,
appBar: new AppBar(
title: const Text('BackgroundFetch Example', style: TextStyle(color: Colors.black)),
backgroundColor: Colors.amberAccent,
actions: <Widget>[
Switch(value: _enabled, onChanged: _onClickEnable),
]
),
body: Container(
child: new ListView.builder(
itemCount: _events.length,
itemBuilder: (BuildContext context, int index) {
DateTime timestamp = _events[index];
return InputDecorator(
decoration: InputDecoration(
contentPadding: EdgeInsets.only(left: 10.0, top: 10.0, bottom: 0.0),
labelStyle: TextStyle(color: Colors.amberAccent, fontSize: 20.0),
labelText: "[background fetch event]"
),
child: new Text(timestamp.toString(), style: TextStyle(color: Colors.white, fontSize: 16.0))
);
}
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
children: <Widget>[
ElevatedButton(onPressed: _onClickStatus, child: Text('Status')),
Container(child: Text("$_status"), margin: EdgeInsets.only(left: 20.0))
]
)
),
),
);
}
}
Dart