You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			238 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Markdown
		
	
			
		
		
	
	
			238 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Markdown
		
	
| # 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.  `Job`s and `Requirement`s 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. |