Moxie Marlinspike bd3d9ac533 Update JobManager README.md
// FREEBIE
2014-11-17 09:17:14 -08:00

7.1 KiB

JobManager

An Android library that facilitates scheduling persistent jobs which are executed when their prerequisites have been met. Similar to Path's android-priority-queue.

The JobManager Way

Android apps often need to perform blocking operations. A messaging app might need to make REST API calls over a network, send SMS messages, download attachments, and interact with a database.

The standard Android way to do these things are with Services, AsyncTasks, or a dedicated Thread. However, some of an app's operations might need to wait until certain dependencies are available (such as a network connection), and some of the operations might need to be durable (complete even if the app restarts before they have a chance to run). The standard Android way can result in a lot of retry logic, timers for monitoring dependencies, and one-off code for making operations durable.

By contrast, the JobManager way allows operations to be broken up into Jobs. A Job represents a unit of work to be done, the prerequisites that need to be met (such as network access) before the work can execute, and the characteristics of the job (such as durable persistence).

Applications construct a JobManager at initialization time:

public class ApplicationContext extends Application {

  private JobManager jobManager;

  @Override
  public void onCreate() {
    initializeJobManager();
  }

  private void initializeJobManager() {
    this.jobManager = JobManager.newBuilder(this)
                                .withName("SampleJobManager")
                                .withConsumerThreads(5)
                                .build();
  }

  ...

}

This constructs a new JobManager with 5 consumer threads dedicated to executing Jobs. A Job looks like this:

public class SampleJob extends Job {

  public SampleJob() {
    super(JobParameters.newBuilder().create());
  }

  @Override
  public onAdded() {
    // Called after the Job has been added to the queue.
  }

  @Override
  public void onRun() {
    // Here's where we execute our work.
    Log.w("SampleJob", "Hello, world!");
  }

  @Override
  public void onCanceled() {
    // This would be called if the job had failed.
  }

  @Override
  public boolean onShouldRetry(Exception exception) {
   // Called if onRun() had thrown an exception to determine whether
   // onRun() should be called again.
   return false;
  }
}

A Job is scheduled simply by adding it to the JobManager:

  this.jobManager.add(new SampleJob());

Persistence

To create durable Jobs, the JobManager needs to be given an interface responsible for serializing and deserializing Job objects. A JavaJobSerializer is included with JobManager that uses Java Serialization, but you can specify your own serializer if you wish:

public class ApplicationContext extends Application {

  private JobManager jobManager;

  @Override
  public void onCreate() {
    initializeJobManager();
  }

  private void initializeJobManager() {
    this.jobManager = JobManager.newBuilder(this)
                                .withName("SampleJobManager")
                                .withConsumerThreads(5)
                                .withJobSerializer(new JavaJobSerializer())
                                .build();
  }

  ...

}

The Job simply needs to declare itself as durable when constructed:

public class SampleJob extends Job {

  public SampleJob() {
    super(JobParameters.newBuilder()
                       .withPersistence()
                       .create());
  }

  ...

Persistent jobs that are enqueued will be serialized to disk to ensure that they run even if the App restarts first. A Job's onAdded() method is called after the commit to disk is complete.

Requirements

A Job might have certain requirements that need to be met before it can run. A requirement is represented by the Requirement interface. Each Requirement must also have a corresponding RequirementProvider that is registered with the JobManager.

A Requirement tells you whether it is present when queried, while a RequirementProvider broadcasts to a listener when a Requirement's status might have changed. Requirement is attached to Job, while RequirementProvider is attached to JobManager.

One common Requirement a Job might depend on is the presence of network connectivity. A NetworkRequirement is bundled with JobManager:

public class ApplicationContext extends Application {

  private JobManager jobManager;

  @Override
  public void onCreate() {
    initializeJobManager();
  }

  private void initializeJobManager() {
    this.jobManager = JobManager.newBuilder(this)
                                .withName("SampleJobManager")
                                .withConsumerThreads(5)
                                .withJobSerializer(new JavaJobSerializer())
                                .withRequirementProviders(new NetworkRequirementProvider(this))
                                .build();
  }

  ...

}

The Job declares itself as having a Requirement when constructed:

public class SampleJob extends Job {

  public SampleJob(Context context) {
    super(JobParameters.newBuilder()
                       .withPersistence()
                       .withRequirement(new NetworkRequirement(context))
                       .create());
  }

  ...

Dependency Injection

It is possible that Jobs (and Requirements) might require dependency injection. A simple example is Context, which many Jobs might require, but can't be persisted to disk for durable Jobs. Or maybe Jobs require more complex DI through libraries such as Dagger.

JobManager has an extremely primitive DI mechanism strictly for injecting Context objects into Jobs and Requirements after they're deserialized, and includes support for plugging in more complex DI systems such as Dagger.

The JobManager Context injection works by having your Job and/or Requirement implement the ContextDependent interface. Jobs and Requirements implementing that interface will get a setContext(Context context) call immediately after the persistent Job or Requirement is deserialized.

To plugin a more complex DI mechanism, simply pass an instance of the DependencyInjector interface to the JobManager:

public class ApplicationContext extends Application implements DependencyInjector {

  private JobManager jobManager;

  @Override
  public void onCreate() {
    initializeJobManager();
  }

  private void initializeJobManager() {
    this.jobManager = JobManager.newBuilder(this)
                                .withName("SampleJobManager")
                                .withConsumerThreads(5)
                                .withJobSerializer(new JavaJobSerializer())
                                .withRequirementProviders(new NetworkRequirementProvider(this))
                                .withDependencyInjector(this)
                                .build();
  }

  @Override
  public void injectDependencies(Object object) {
    // And here we do our DI magic.
  }

  ...

}

injectDependencies(Object object) will be called for a Job before the job's onAdded() method is called, or after a persistent job is deserialized.