How to Create Background Tasks in Ionic with Capacitor

By admin

Copied
ORG

This is a guest post from

Simon Grimm
ORG

,

Ionic Developer Expert
ORG

and educator at

the Ionic Academy
ORG

, an online school with

70
CARDINAL

+ video courses focused entirely on building awesome mobile apps with

Ionic
ORG

and Capacitor!

What happens when users put your mobile app in the background? Can you still occasionally tap into the device’s resources to perform tasks? The answer is yes, and Capacitor makes it easy to do so now with the new

Capacitor Background Runner
PRODUCT

.

In this tutorial, we will learn how to use the new

Background Runner
PRODUCT

plugin to perform tasks such as fetching data, sending notifications, or even getting the user’s geolocation. We will also learn how to debug and trigger background tasks directly from

Xcode
ORG

.

You can also find the full source code on GitHub. But before we get into the plugin, let’s

first
ORDINAL

take a look at what background tasks are and why we need them.

Background Tasks in Mobile Apps

When building mobile apps, we often have to deal with tasks that should run in the background, like checking for new emails, updating chat messages, or syncing data. In the past, this was only possible with native code, but now we can do it with Capacitor as well.

The idea is to define a task that you will run in the background and either dispatch it manually from your code or let the OS trigger it automatically on a given interval.

However, the

second
ORDINAL

option proves very challenging in reality. Because even if you tell iOS to perform your background task

every 5 minutes
TIME

, your code will most likely get executed

every 2 hours
TIME

.

This is because iOS will try to optimize the battery life of the device and only run background tasks when it thinks it’s a good time to do so. This is why you should never rely on background tasks to run at a specific time.

However, you can still use this to update your app’s data before a user opens it again or to send a local notification to the user.

It can also offload some work from the main thread to the background thread – and we will see all of that in action.

If you want to learn more about this, check out

the Capacitor Livestream
WORK_OF_ART

about Background Tasks!

Setting up the Project with Background Runner

To get started, bring up a terminal, create a new

Ionic
ORG

app, and install

the Capacitor Background Runner
PRODUCT

:

ionic start backgroundApp blank –type angular cd ./backgroundApp npm install @capacitor/background-runner ionic cap add ios ionic cap add

android
ORG

Next, we need to add a file that holds our background tasks, so simply create one at src/app/runners/runner.js and leave it empty for the moment. To make sure this file is copied when building the app, we need to bring up the

angular.json
FAC

and include it in the assets array like this:

{ "projects": { "app": { "architect": { "build": { "options": { "assets": [ { "glob": "**/*", "input": "src/assets", "output": "assets" }, { "glob": "**/*.svg", "input": "node_modules/ionicons/dist/ionicons/svg", "output": "./svg" }, { "glob": "runner.js", "input": "src/app/runners", "output": "./runners" } ] } } } } } }

Now we also need to specify some information for Capacitor so the plugin can pick up the file and eventually an event that we can trigger from our code. To do so, open the capacitor.config.ts and change it like this:

import {

CapacitorConfig
ORG

} from ‘@capacitor/cli’; const config:

CapacitorConfig
ORG

= { appId: ‘com.capacitor.background’, appName: ‘backgroundApp’, webDir: ‘www’, server: { androidScheme: ‘

https
PERSON

‘, }, plugins: {

BackgroundRunner
PERSON

: { label: ‘com.capacitor.background.check’, src: ‘runners/runner.js’, event: ‘checkIn’, repeat: true, interval:

30
CARDINAL

, autoStart: true, }, }, }; export default config;

The most important part is that the plugin has a similar label to your appId! This is important because otherwise, the plugin won’t be able to find your file or execute the task. The time we specify here is more a friendly request to the OS as mentioned before, we are not guaranteed to get executed at this time.


Native App Configuration
ORG

To make sure our app can run in the background, we need to add some configuration to our native apps as well.

iOS

For iOS we

first
ORDINAL

need to update the ios/App/App/AppDelegate.swift file and change

one
CARDINAL

function and add another:

import

UIKit
PRODUCT

import

BackgroundTasks
PRODUCT

import Capacitor import CapacitorBackgroundRunner @UIApplicationMain class

AppDelegate
ORG

: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application:

UIApplication
ORG

, didFinishLaunchingWithOptions launchOptions: [

UIApplication
ORG

.LaunchOptionsKey: Any]?) -> Bool { BackgroundRunnerPlugin.registerBackgroundTask() BackgroundRunnerPlugin.handleApplicationDidFinishLaunching(launchOptions: launchOptions) return true } func application(_ application:

UIApplication
ORG

, didReceiveRemoteNotification userInfo: [

AnyHashable
WORK_OF_ART

: Any], fetchCompletionHandler completionHandler:

@escaping
PERSON

(UIBackgroundFetchResult) -> Void) { print("Received remote notification") BackgroundRunnerPlugin.dispatchEvent(event: "remoteNotification", eventArgs: userInfo) { result in switch result { case .success: completionHandler(.newData) case .failure: completionHandler(.failed) } } } }

Additionally, we need to enable the background capability and different modes that our app will use from the background:

Select the modes as you see fit, but for our example, these are the ones we need:

Now we also need to include some permissions in the ios/App/App/Info.plist for geolocation and also add the BGTaskSchedulerPermittedIdentifiers if it wasn’t added before and use the key of your background task:

<key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>com.capacitor.background.check</string> </array> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>$(PRODUCT_NAME) uses location services to track your location.</string> <key>

NSLocationAlwaysUsageDescription</key
ORG

> <string>$(PRODUCT_NAME) uses location services to track your location.</string> <key>NSLocationWhenInUseUsageDescription</key> <string>$(PRODUCT_NAME) uses location services to track your location.</string>

This did not happen automatically for me, so check your file for that entry.


Android
ORG

For

Android
ORG

we

first
ORDINAL

need to add some permissions as well, so open up the android/app/src/main/AndroidManifest.xml and add the following:

<uses-permission

android
ORG

:name="android.permission.

ACCESS_COARSE_LOCATION
EVENT

" /> <uses-permission

android
ORG

:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission

android
ORG

:name="android.permission.

SCHEDULE_EXACT_ALARM
NORP

" /> <uses-feature android:name="android.hardware.location.gps" />

There are no other changes needed for

Android
ORG

, so we are good to go!

Implementing the Background Task

Let’s start with an easy example and simply save a value to the

CapacitorKV
ORG

store. This is like the Capacitor Preferences plugin and is directly available to our Background Runner. To do so, open up the src/app/runners/runner.js and add the following code:

// Save a value to the Capacitor KV store

addEventListener(‘testSave
PRODUCT

‘, async (resolve, reject, args) => { try { CapacitorKV.set(‘foo’, ‘my bar 42’); resolve(); } catch (err) { console.error(err); reject(err); } }); // Get a value from the Capacitor KV store addEventListener(‘testLoad’, async (resolve, reject, args) => { try { const value = CapacitorKV.get(‘foo’); resolve(value); } catch (err) { console.error(err); reject(err); } });

This is a very simple example, but it shows how we can use

the Capacitor API
PRODUCT

directly from our background task. Now that we have

two
CARDINAL

events defined, we can call them directly from our app! For this, bring up the src/app/home/home.page.ts and

first
ORDINAL

request permissions for our background runner and then call the dispatchEvent() method to trigger the event:

import { Component } from ‘@angular/core’; import {

BackgroundRunner
PERSON

} from ‘@capacitor/background-runner’;

@Component
PERSON

({ selector: ‘app-home’, templateUrl: ‘home.page.html’, styleUrls: [‘home.page.scss’], }) export class HomePage { user: any = null; constructor() { this.init(); } // Request permissions for background tasks async init() { try { const permissions = await BackgroundRunner.requestPermissions({ apis: [‘notifications’, ‘geolocation’], }); console.log(‘permissions’, permissions); } catch (err) { console.log(`ERROR: ${err}`); } } // Test

the KV Store
ORG

async testSave() { const result = await BackgroundRunner.dispatchEvent({ label: ‘com.capacitor.background.check’, event: ‘testSave’, details: {}, }); console.log(‘save result’, result); } async testLoad() { const result = await BackgroundRunner.dispatchEvent({ label: ‘com.capacitor.background.check’, event: ‘testLoad’, details: {}, }); console.log(‘load result’, result); } }

Now simply call those functions with some buttons and check your logs when you run the app on a real device – you should see the values being saved and loaded!

Using Background Notifications and Data Fetching

The real power of background tasks comes from the fact that we can offload some work from our app, or periodically update some data. Let’s now add a fetch call that returns data, and another call to the

CapacitorNotifications
ORG

plugin to schedule a local notification.

Bring up the src/app/runners/runner.js again and add:

// Make a fetch request to

the randomuser API
PERSON

and return

first
ORDINAL

user addEventListener(‘fetchTest’, async (resolve, reject, args) => { try { const res = await

fetch(‘https://randomuser.me
GPE

/api/’); if (!res.ok) { throw new Error(‘Could not fetch user’); } const result = await

res.json
PERSON

(); resolve(result[‘results’][0]); } catch (err) { console.error(err); reject(err); } }); // Trigger a local notification addEventListener(‘notificationTest’, async (resolve, reject, args) => { try { let scheduleDate = new Date();

scheduleDate.setSeconds(scheduleDate.getSeconds
PERSON

() + 5); CapacitorNotifications.schedule([ { id:

42
CARDINAL

, title: ‘

Background Magic
WORK_OF_ART

🧙‍♂️’, body: ‘This comes from the background runner’, scheduleAt: scheduleDate, }, ]); resolve(); } catch (err) { console.error(err); reject(err); } });

Again we can simply include those events in our src/app/home/home.page.ts like this:

// Test the background fetch async performBackgroundFetch() { const result = await BackgroundRunner.dispatchEvent({ label: ‘com.capacitor.background.check’, event: ‘fetchTest’, details: {}, }); this.user = result; }; // Schedule a notification from background async scheduleNotification() { await BackgroundRunner.dispatchEvent({ label: ‘com.capacitor.background.check’, event: ‘notificationTest’, details: {}, }); };

And finally, update your

UI
ORG

inside the src/app/home/home.page.html to show the buttons and an item that we try to load from the background:

<ion-header> <ion-toolbar> <ion-title> Capacitor Background </ion-title> </ion-toolbar> </ion-header> <ion-content> <ion-button (click)="performBackgroundFetch()" expand="full" >Perform Background Call</ion-button> <ion-button (click)="scheduleNotification()" expand="full" >Schedule Local Notification</ion-button> <ion-button (click)="testSave()" expand="full">Save values</ion-button> <ion-button (click)="testLoad()" expand="full">Load values</ion-button> <ion-button routerLink="/map" expand="full">Map</ion-button> <ion-item *ngIf="user"> <ion-avatar slot="start"> <img [src]="user.picture.thumbnail" /> </ion-avatar> <ion-label> <h2>{{ user.name.first }} {{user.name.last}}</h2> <p>{{ user.email }}</p> </ion-label> </ion-item> </ion-content>

Try and trigger the background fetch!

We have now successfully triggered a background task that fetches data and returns it to our app – a huge milestone! You can now also try to schedule a notification and see it appear on your device.

All of this works flawlessly if you have configured everything correctly in the

first
ORDINAL

step. But let’s take this another step further.

Adding Capacitor Google Maps

We could now combine our background check-in with the Capacitor Google Maps Plugin to capture the geolocation, store it and later show it on a map!

To do so,

first
ORDINAL

install the plugin and create a new page:

ionic g page map npm install @capacitor/google-maps

To configure the plugin you need to supply your

Google Maps API
PRODUCT

key inside the android/app/src/main/AndroidManifest.xml like this:

<application …> <meta-data

android
ORG

:name="com.google.android.geo.

API_KEY
PERSON

"

android
ORG

:value="YOURAPIKEY"/> </application>

For more information about the integration also check out my Capacitor Maps tutorial on

the Ionic Academy
ORG

! Now we can add

two
CARDINAL

new functions to our runner file to save and load the check-ins:

// Save a time and location object in the Capacitor KV store addEventListener(‘checkIn’, async (resolve, reject, args) => { try { console.log(‘checkIn event fired’); const { value } = CapacitorKV.get(‘CHECKINS’); // Gather some data const time = new

Date().getTime
PRODUCT

(); const location = await

CapacitorGeolocation.getCurrentPosition
FAC

(); // Create an array of checkins let checkinArr = [{ location, time }]; // Try to append our data to the existing array try { const parsedArr = JSON.parse(value); checkinArr = […parsedArr, { location, time }]; } catch (e) { console.log(‘no checkins’); } console.log(checkinArr); // Save the array CapacitorKV.set(‘CHECKINS’, JSON.stringify(checkinArr));

console.log(‘checkin
PERSON

saved’); // Resolve the event call resolve(); } catch (err) { console.error(err); reject(err); } }); // Get all checkins from the Capacitor KV store addEventListener(‘loadCheckins’, (resolve, reject, args) => { try { const { value } = CapacitorKV.get(‘CHECKINS’); try { const arr = JSON.parse(value);

resolve(arr
PERSON

); } catch (e) { resolve([]); } } catch (err) { console.error(err); reject([]); } });

We have previously defined in our Capacitor configuration that the checkIn event will be automatically scheduled when our app goes to the background – if you use different event names, make sure you update your config. To finish the maps implementation, bring up the src/app/map/map.module.ts and include the

CUSTOM_ELEMENTS_SCHEMA
ORG

import { NgModule } from ‘@angular/core’; import {

CommonModule
ORG

} from ‘@angular/common’; import {

FormsModule
ORG

} from ‘@angular/forms’; import { IonicModule } from ‘@ionic/angular’; import { MapPageRoutingModule } from ‘./map-routing.module’; import { MapPage } from ‘./map.page’; import {

CUSTOM_ELEMENTS_SCHEMA
ORG

} from ‘@angular/core’;

@NgModule
PERSON

({ imports: [

CommonModule
ORG

,

FormsModule
ORG

, IonicModule, MapPageRoutingModule], declarations: [MapPage], schemas: [

CUSTOM_ELEMENTS_SCHEMA
ORG

], }) export class MapPageModule {}

Finally, sprinkle in some CSS inside the src/app/map/map.page.scss to make the map view visible on native devices:

capacitor-google-map { display: inline-block; width:

100%
PERCENT

; height: 400px; } ion-content { –background: none; }

Now we can use the map in our page and also display a list with the check-ins below it, so change your src/app/map/map.page.html to:

<ion-header> <ion-toolbar> <ion-buttons slot="start"> <ion-back-button defaultHref="home"></ion-back-button> </ion-buttons> <ion-title>Map</ion-title> </ion-toolbar> </ion-header> <ion-content> <ion-button (click)="loadCheckins()" expand="full">Load Checkins</ion-button> <capacitor-google-map #map></capacitor-google-map> <ion-list> <ion-item *ngFor="let checkin of checkins"> <ion-label> <h2>{{ checkin.location?.longitude }} – {{ checkin.location?.latitude }}</h2> <p>{{ checkin.time | date:’medium’ }}</p> </ion-label> </ion-item> </ion-list> </ion-content>

Finally in the src/app/map/map.page.ts we need to initialize the map and then load the check-ins from the background runner using dispatchEvent() again.

When we have the data, we can add markers to the map and display them:

import { AfterViewInit,

Component
ORG

, ElementRef,

ViewChild
PERSON

} from ‘@angular/core’; import {

BackgroundRunner
PERSON

} from ‘@capacitor/background-runner’; import {

GoogleMap
ORG

,

Marker
PERSON

} from ‘@capacitor/google-maps’;

@Component
PERSON

({ selector: ‘app-map’, templateUrl: ‘./map.page.html’, styleUrls: [‘./map.page.scss’], }) export class

MapPage
PRODUCT

implements AfterViewInit { @ViewChild(‘map’) mapRef!: ElementRef<HTMLElement>; map!:

GoogleMap
PERSON

; checkins: any[] = []; constructor() {} ngAfterViewInit() { this.createMap(); } async createMap() { this.map = await GoogleMap.create({ id: ‘my-cool-map’, element: this.mapRef.nativeElement,

apiKey
ORG

: ‘

YOURAPIKEY
GPE

‘, config: { center: { lat:

51.88
CARDINAL

, lng:

7.6
CARDINAL

, }, zoom:

8
CARDINAL

, }, }); } loadCheckins = async () => { const result = (await BackgroundRunner.dispatchEvent({ label: ‘com.capacitor.background.check’, event: ‘

loadCheckins
PERSON

‘, details: {}, })) as any; if (result) { this.checkins = []; Object.keys(result).forEach((key) => { this.checkins.push(result[key]); }); this.addMarkers(); } }; async

addMarkers
PERSON

() { if (this.checkins.length === 0) { return; } const markers: Marker[] = this.checkins.map((item) => { return { coordinate: { lat: item.location.latitude, lng: item.location.longitude, }, title: item.time, }; }); await this.map.addMarkers(markers); } }

Once your background task has run, you should see the markers on the map!

But what if it’s not running and you don’t want to wait until it’s executed?

Debug and Trigger Background Tasks

For iOS, there is a handy trick to manually trigger your background tasks.


First
ORDINAL

, we will add a random print statement to the

BackgroundRunner
PERSON

file in our pods target after the BGTaskScheduler.shared.submit() call.

Set a breakpoint to the left of that line of code, run your app, and slide it to the background – your breakpoint will be triggered.

Now you can run the following command in the log window where you see (lldb):

e -l objc — (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.academy.runner.check"]

Replace the identifier with your own and hit enter.

This causes iOS to schedule and run your background task when the app continues, so let the app continue and you should see a log from your runner.

If you bring up the app again, you should also see the check-ins on the map!

Apparently, the only debugging option for

Android
ORG

I found was to use the App Inspector view in

Android Studio
ORG

, from which you can at least get a glimpse of your background job:

If you find a way to directly trigger the

Android
ORG

background task, let me know!

Recap

In this tutorial we have learned how to use the new

Capacitor Background Runner
PRODUCT

plugin to perform tasks in the background, like fetching data, sending notifications, or even getting the user’s geolocation.

We have access to a limited set of APIs already, but we need to make sure we request the permissions upfront and configure our native apps correctly. To see all of them in action, also check out the official

Capacitor Background Runner
PRODUCT

demo app!

Keep in mind the limitations of background tasks which are quite heavily restricted on both iOS and

Android
ORG

. But if you act within the boundaries of the operating systems, adding background tasks to your

Ionic
ORG

app with Capacitor is now possible and right at your fingertips!