Firebase Job Dispatcher
This guide discusses how to schedule background work using the Firebase Job Dispatcher library from Google.
Overview
One of the best ways to keep an Android application responsive to the user is to ensure that complex or long running work is performed in the background. However, it is important that background work will not negatively impact the user's experience with the device.
For example, a background job might poll a website every three or four minutes to query for changes to a particular dataset. This seems benign, however it would have a disastrous impact on battery life. The application will repeatedly wake up the device, elevate the CPU to a higher power state, power up the radios, make the network requests, and then processing the results. It gets worse because the device will not immediately power down and return to the low-power idle state. Poorly scheduled background work may inadvertently keep the device in a state with unnecessary and excessive power requirements. This seemingly innocent activity (polling a website) will render the device unusable in a relatively short period of time.
Android provides the following APIs to help with performing work in the background but by themselves they are not sufficient for intelligent job scheduling.
- Intent Services – Intent Services are great for performing the work, however they provide no way to schedule work.
- AlarmManager – These APIs only allow work to be scheduled but provide no way to actually perform the work. Also, the AlarmManager only allows time based constraints, which means raise an alarm at a certain time or after a certain period of time has elapsed.
- JobScheduler – The JobSchedule is a great API that works with the operating system to schedule jobs. However, it is only available for those Android apps that target API level 21 or higher.
- Broadcast Receivers – An Android app can setup broadcast receivers to perform work in response to system-wide events or Intents. However, broadcast receivers don't provide any control over when the job should be run. Also changes in the Android operating system will restrict when broadcast receivers will work, or the kinds of work that they can respond to.
There are two key features to efficiently performing background work (sometimes referred to as a background job or a job):
- Intelligently scheduling the work – It is important that when an application is doing work in the background that it does so as a good citizen. Ideally, the application should not demand that a job be run. Instead, the application should specify conditions that must be met for when the job can run, and then schedule that work to run when the conditions are met. This allows Android to intelligently perform work. For example, network requests may be batched to run all at the same time to make maximum use of overhead involved with networking.
- Encapsulating the work – The code to perform the background work should be encapsulated in a discrete component that can be run independently of the user interface and will be relatively easy to reschedule if the work fails to complete for some reason.
The Firebase Job Dispatcher is a library from Google that provides a fluent API to simplify scheduling background work. It is intended to be the replacement for Google Cloud Manager. The Firebase Job Dispatcher consists of the following APIs:
- A
Firebase.JobDispatcher.JobService
is an abstract class that must be extended with the logic that will run in the background job. - A
Firebase.JobDispatcher.JobTrigger
declares when the job should be started. This is typically expressed as a window of time, for example, wait at least 30 seconds before starting the job, but run the job within 5 minutes. - A
Firebase.JobDispatcher.RetryStrategy
contains information about what should be done when a job fails to execute properly. The retry strategy specifies how long to wait before trying to run the job again. - A
Firebase.JobDispatcher.Constraint
is an optional value that describes a condition that must be met before the job can run, such as the device is on an unmetered network or charging. - The
Firebase.JobDispatcher.Job
is an API that unifies the previous APIs in to a unit-of-work that can be scheduled by theJobDispatcher
. TheJob.Builder
class is used to instantiate aJob
. - A
Firebase.JobDispatcher.JobDispatcher
uses the previous three APIs to schedule the work with the operating system and to provide a way to cancel jobs, if necessary.
To schedule work with the Firebase Job Dispatcher, a Xamarin.Android application must encapsulate the code in a type that extends the JobService
class. JobService
has three lifecycle methods that can be called during the lifetime of the job:
bool OnStartJob(IJobParameters parameters)
– This method is where the work will occur and should always be implemented. It runs on the main thread. This method will returntrue
if there is work remaining, orfalse
if the work is done.bool OnStopJob(IJobParameters parameters)
– This is called when the job is stopped for some reason. It should returntrue
if the job should be rescheduled for later.JobFinished(IJobParameters parameters, bool needsReschedule)
– This method is called when theJobService
has finished any asynchronous work.
To schedule a job, the application will instantiate a JobDispatcher
object. Then, a Job.Builder
is used to create a Job
object, which is provided to the JobDispatcher
which will try and schedule the job to run.
This guide will discuss how to add the Firebase Job Dispatcher to a Xamarin.Android application and use it to schedule background work.
Requirements
The Firebase Job Dispatcher requires Android API level 9 or higher. The Firebase Job Dispatcher library relies on some components provided by Google Play Services; the device must have Google Play Services installed.
Using the Firebase Job Dispatcher Library in Xamarin.Android
To get started with the Firebase Job Dispatcher, first add the Xamarin.Firebase.JobDispatcher NuGet package to the Xamarin.Android project. Search the NuGet Package Manager for the Xamarin.Firebase.JobDispatcher package (which is still in pre-release).
After adding the Firebase Job Dispatcher library, create a JobService
class and then schedule it to run with an instance of the FirebaseJobDispatcher
.
Creating a JobService
All work performed by the Firebase Job Dispatcher library must be done in a type that extends the Firebase.JobDispatcher.JobService
abstract class. Creating a JobService
is very similar to creating a Service
with the Android framework:
- Extend the
JobService
class - Decorate the subclass with the
ServiceAttribute
. Although not strictly required, it is recommended to explicitly set theName
parameter to help with debugging theJobService
. - Add an
IntentFilter
to declare theJobService
in the AndroidManifest.xml. This will also help the Firebase Job Dispatcher library locate and invoke theJobService
.
The following code is an example of the simplest JobService
for an application, using the TPL to asynchronously perform some work:
[Service(Name = "com.xamarin.fjdtestapp.DemoJob")]
[IntentFilter(new[] {FirebaseJobServiceIntent.Action})]
public class DemoJob : JobService
{
static readonly string TAG = "X:DemoService";
public override bool OnStartJob(IJobParameters jobParameters)
{
Task.Run(() =>
{
// Work is happening asynchronously (code omitted)
});
// Return true because of the asynchronous work
return true;
}
public override bool OnStopJob(IJobParameters jobParameters)
{
Log.Debug(TAG, "DemoJob::OnStartJob");
// nothing to do.
return false;
}
}
Creating a FirebaseJobDispatcher
Before any work can be scheduled, it is necessary to create a Firebase.JobDispatcher.FirebaseJobDispatcher
object. The FirebaseJobDispatcher
is responsible for scheduling a JobService
. The following code snippet is one way to create an instance of the FirebaseJobDispatcher
:
// This is the "Java" way to create a FirebaseJobDispatcher object
IDriver driver = new GooglePlayDriver(context);
FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(driver);
In the previous code snippet, the GooglePlayDriver
is class that helps the FirebaseJobDispatcher
interact with some of the scheduling APIs in Google Play Services on the device. The parameter context
is any Android Context
, such as an Activity. Currently the GooglePlayDriver
is the only IDriver
implementation in the Firebase Job Dispatcher library.
The Xamarin.Android binding for the Firebase Job Dispatcher provides an extension method to create a FirebaseJobDispatcher
from the Context
:
FirebaseJobDispatcher dispatcher = context.CreateJobDispatcher();
Once the FirebaseJobDispatcher
has been instantiated, it is possible to create a Job
and run the code in the JobService
class. The Job
is created by a Job.Builder
object and will be discussed in the next section.
Creating a Firebase.JobDispatcher.Job with the Job.Builder
The Firebase.JobDispatcher.Job
class is responsible for encapsulating the meta-data necessary to run a JobService
. AJob
contains information such as any constraint that must be met before the job can run, if the Job
is recurring, or any triggers that will cause the job to be run. As a bare minimum, a Job
must have a tag (a unique string that identifies the job to the FirebaseJobDispatcher
) and the type of the JobService
that should be run. The Firebase Job Dispatcher will instantiate the JobService
when it is time to run the job. A Job
is created by using an instance of the Firebase.JobDispatcher.Job.JobBuilder
class.
The following code snippet is the simplest example of how to create a Job
using the Xamarin.Android binding:
Job myJob = dispatcher.NewJobBuilder()
.SetService<DemoJob>("demo-job-tag")
.Build();
The Job.Builder
will perform some basic validation checks on the input values for the job. An exception will be thrown if it not possible for the Job.Builder
to create a Job
. The Job.Builder
will create a Job
with the following defaults:
- A
Job
's lifetime (how long it will be scheduled to run) is only until the device reboots – once the device reboots theJob
is lost. - A
Job
is not recurring – it will only run once. - A
Job
will be scheduled to run as soon as possible. - The default retry strategy for a
Job
is to use an exponential backoff (discussed on more detail below in the section Setting a RetryStrategy)
Scheduling a job
After creating the Job
, it needs to be scheduled with the FirebaseJobDispatcher
before it is run. There are two methods for scheduling a Job
:
// This will throw an exception if there was a problem scheduling the job
dispatcher.MustSchedule(myJob);
// This method will not throw an exception; an integer result value is returned
int scheduleResult = dispatcher.Schedule(myJob);
The value returned by FirebaseJobDispatcher.Schedule
will be one of the following integer values:
FirebaseJobDispatcher.ScheduleResultSuccess
– TheJob
was successfully scheduled.FirebaseJobDispatcher.ScheduleResultUnknownError
– Some unknown problem occurred which prevented theJob
from being scheduled.FirebaseJobDispatcher.ScheduleResultNoDriverAvailable
– An invalidIDriver
was used or theIDriver
was somehow unavailable.FirebaseJobDispatcher.ScheduleResultUnsupportedTrigger
– TheTrigger
was not supported.FirebaseJobDispatcher.ScheduleResultBadService
– The service is not configured correctly or is unavailable.
Configuring a job
It is possible to customize a job. Examples of how a job may be customized include the following:
- Passing Parameters to a Job – A
Job
may require additional values to perform its work, for example downloading a file. - Set Constraints – It may be necessary to only run a job when certain conditions are met. For example, only run a
Job
when the device is charging. - Specify when a
Job
should run – The Firebase Job Dispatcher allows applications to specify a time when the job should run. - Declare a retry strategy for failed jobs – A retry strategy provides guidance to the
FirebaseJobDispatcher
on what to do withJobs
that fail to complete.
Each of these topics will be discussed more in the following sections.
Passing parameters to a job
Parameters are passed to a job by creating a Bundle
that is passed along with the Job.Builder.SetExtras
method:
Bundle jobParameters = new Bundle();
jobParameters.PutInt(FibonacciCalculatorJob.FibonacciPositionKey, 25);
Job myJob = dispatcher.NewJobBuilder()
.SetService<DemoJob>("demo-job-tag")
.SetExtras(jobParameters)
.Build();
The Bundle
is accessed from the IJobParameters.Extras
property on the OnStartJob
method:
public override bool OnStartJob(IJobParameters jobParameters)
{
int position = jobParameters.Extras.GetInt(FibonacciPositionKey, DEFAULT_VALUE);
// rest of code omitted
}
Setting constraints
Constraints can help reduces costs or battery drain on the device. The Firebase.JobDispatcher.Constraint
class defines these constraints as integer values:
Constraint.OnUnmeteredNetwork
– Only run the job when the device is connected to an unmetered network. This is useful to prevent the user from incurring data charges.Constraint.OnAnyNetwork
– Run the job on whatever network the device is connected to. If specified along withConstraint.OnUnmeteredNetwork
, this value will take priority.Constraint.DeviceCharging
– Run the job only when the device is charging.
Constraints are set with the Job.Builder.SetConstraint
method:
Job myJob = dispatcher.NewJobBuilder()
.SetService<DemoJob>("demo-job-tag")
.SetConstraint(Constraint.DeviceCharging)
.Build();
The JobTrigger
provides guidance to the operating system about when the job should start. A JobTrigger
has an executing window that defines a scheduled time for when the Job
should run. The execution window has a start window value and an end window value. The start window is the number of seconds that the device should wait before running the job and the end window value is the maximum number of seconds to wait before running the Job
.
A JobTrigger
can be created with the Firebase.Jobdispatcher.Trigger.ExecutionWindow
method. For example Trigger.ExecutionWindow(15,60)
means that the job should run between 15 and 60 seconds from when it is scheduled. The Job.Builder.SetTrigger
method is used to
JobTrigger myTrigger = Trigger.ExecutionWindow(15,60);
Job myJob = dispatcher.NewJobBuilder()
.SetService<DemoJob>("demo-job-tag")
.SetTrigger(myTrigger)
.Build();
The default JobTrigger
for a job is represented by the value Trigger.Now
, which specifies that a job be run as soon as possible after being scheduled..
Setting a RetryStrategy
The Firebase.JobDispatcher.RetryStrategy
is used to specify how much of a delay a device should use before trying to re-run a failed job. A RetryStrategy
has a policy, which defines what time-base algorithm will be used to re-schedule the failed job, and an execution window that specifies a window in which the job should be scheduled. This rescheduling window is defined by two values. The first value is the number of seconds to wait before rescheduling the job (the initial backoff value), and the second number is the maximum number of seconds before the job must run (the maximum backoff value).
The two types of retry policies are identified by these int values:
RetryStrategy.RetryPolicyExponential
– An exponential backoff policy will increase the initial backoff value exponentially after each failure. The first time a job fails, the library will wait the _initial interval that is specified before rescheduling the job – example 30 seconds. The second time the job fails, the library will wait at least 60 seconds before trying to run the job. After the third failed attempt, the library will wait 120 seconds, and so on. The defaultRetryStrategy
for the Firebase Job Dispatcher library is represented by theRetryStrategy.DefaultExponential
object. It has an initial backoff of 30 seconds and a maximum backoff of 3600 seconds.RetryStrategy.RetryPolicyLinear
– This strategy is a linear backoff that the job should be rescheduled to run at set intervals (until it succeeds). Linear backoff is best suited for work that must be completed as soon as possible or for problems that will quickly resolve themselves. The Firebase Job Dispatcher library defines aRetryStrategy.DefaultLinear
which has a rescheduling window of at least 30 seconds and up to 3600 seconds.
It is possible to define a custom RetryStrategy
with the FirebaseJobDispatcher.NewRetryStrategy
method. It takes three parameters:
int policy
– The policy is one of the previousRetryStrategy
values,RetryStrategy.RetryPolicyLinear
, orRetryStrategy.RetryPolicyExponential
.int initialBackoffSeconds
– The initial backoff is a delay, in seconds, that is required before trying to run the job again. The default value for this is 30 seconds.int maximumBackoffSeconds
– The maximum backoff value declares the maximum number of seconds to delay before trying to run the job again. The default value is 3600 seconds.
RetryStrategy retry = dispatcher.NewRetryStrategy(RetryStrategy.RetryPolicyLinear, initialBackoffSeconds, maximumBackoffSet);
// Create a Job and set the RetryStrategy via the Job.Builder
Job myJob = dispatcher.NewJobBuilder()
.SetService<DemoJob>("demo-job-tag")
.SetRetryStrategy(retry)
.Build();
Cancelling a job
It is possible to cancel all the jobs that have been scheduled, or just a single job using the FirebaseJobDispatcher.CancelAll()
method or the FirebaseJobDispatcher.Cancel(string)
method:
int cancelResult = dispatcher.CancelAll();
// to cancel a single job:
int cancelResult = dispatcher.Cancel("unique-tag-for-job");
Either method will return an integer value:
FirebaseJobDispatcher.CancelResultSuccess
– The job was successfully cancelled.FirebaseJobDispatcher.CancelResultUnknownError
– An error prevented the job from being cancelled.FirebaseJobDispatcher.CancelResult.NoDriverAvailable
– TheFirebaseJobDispatcher
is unable to cancel the job as there is no validIDriver
available.
Summary
This guide discussed how to use the Firebase Job Dispatcher to intelligently perform work in the background. It discussed how to encapsulate the work to be performed as a JobService
and how to use the FirebaseJobDispatcher
to schedule that work, specifying the criteria with a JobTrigger
and how failures should be handled with a RetryStrategy
.