Background Fetch In Flutter Android and IOS Setup

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);
  }
}
Java

iOS 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

  1. Open your Info.plist and add the key “Permitted background task scheduler identifiers”
  1. Add the required identifier com.transistorsoft.fetch.
  1. 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 custom taskId: 'com.transistorsoft.customtask', you must add the identifier com.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) 
));
Dart

Steps

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;
  }
Dart

backgroundFetchHeadlessTask

[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);
}
Dart

Start 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');
      });
      
Dart

Full 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