Upgrade to Pro — share decks privately, control downloads, hide ads and more …

From AlarmManager to WorkManager

Ralf
August 27, 2018

From AlarmManager to WorkManager

Android has a variety of tools to deal with background work. Android provides different APIs with the AlarmManager and JobScheduler, but there are also many third party libraries like android-job and Firebase JobDispatcher. And more recently, Android architecture components introduced the WorkManager. How does this new component fit into the existing APIs and which gaps does it try to fill?

This talk gives an overview of the various job scheduler engines and how their capabilities have evolved alongside mobile ecosystems. The different APIs will be discussed and most importantly we will cover some best practices to avoid repeating yourself.

Ralf

August 27, 2018
Tweet

More Decks by Ralf

Other Decks in Programming

Transcript

  1. AlarmManager Available on all devices Uses PendingIntent to send broadcasts

    at a given time Doesn’t respect the device state 2008 2018 2013 2014 2015 2016 2017
  2. AlarmManager With KitKat the API behavior changed New API for

    scheduling exact jobs 2008 2018 2013 2014 2015 2016 2017
  3. JobScheduler Fluent API Respects device state Only on API 21+

    Platform bugs 2008 2018 2013 2014 2015 2016 2017
  4. GcmNetworkManager 2008 2018 2013 2014 2015 2016 2017 Similar API

    like JobScheduler API 9+ Part of the Play Services SDK
  5. android-job 2008 2018 2013 2014 2015 2016 2017 Single API

    to use the AlarmManager, JobScheduler and GcmNetworkManager Many features backported
  6. Firebase JobDispatcher 2008 2018 2013 2014 2015 2016 2017 Same

    API as JobScheduler Wrapper for job schedule engines, but only supports the GcmNetworkManager
  7. Android Oreo 2008 2018 2013 2014 2015 2016 2017 Background

    service limitations Broadcast removal
  8. JobIntentService 2008 2018 2013 2014 2015 2016 2017 Replacement for

    IntentService Respects app state Only for running jobs immediately
  9. WorkManager 2008 2018 2013 2014 2015 2016 2017 Single API

    to use the AlarmManager, JobScheduler or JobDispatcher Architecture component and part of Android Jetpack
  10. Which API or component should you use? It depends In

    the short term probably android-job In the long term probably WorkManager
  11. Which API or component should you use? AlarmManager JobScheduler IntentService

    GcmNetworkManager Firebase JobDispatcher android-job WorkManager JobIntentService API 26+ API 23+ API 21+ Optional API 23+ Optional
  12. Wrapper around existing APIs Many feature backported to API 14

    Automatically chooses best API for your requirements Many features: delayed or instant jobs, periodic jobs, exact jobs, unique jobs, daily jobs, no reflection, many requirements, back-off criteria, extras, transient extras, wake-locks, background threads, … android-job
  13. dependencies { implementation 'com.evernote:android-job:1.3.0-alpha06' // 1.2.6 current stable } class

    App : Application() { override fun onCreate() { super.onCreate() JobManager.create(this).addJobCreator(ImageUploadJobCreator) } } Setup
  14. object ImageUploadJobCreator : JobCreator { override fun create(tag: String): Job?

    { return when (tag) { ImageUploadJob.TAG -> ImageUploadJob() SyncJob.TAG -> SyncJob() else -> null } } } Setup
  15. class ImageUploadJob : Job() { override fun onRunJob(params: Params): Result

    { val imageId = params.extras.getInt(EXTRA_IMAGE, -1) if (imageId <= 0) { return Result.FAILURE } // do something return Result.SUCCESS } } Sample Job
  16. class ImageUploadJob : Job() { companion object { const val

    TAG = "ImageUploadJob" private const val EXTRA_IMAGE = "EXTRA_IMAGE" fun schedule(image: Image) { return JobRequest.Builder(TAG) .startNow() .setExtras(PersistableBundleCompat().apply { putInt(EXTRA_IMAGE, image.id) }) .build() .scheduleAsync() } } } Sample Job
  17. Wrapper around existing APIs Many feature backported to API 14

    Automatically chooses best API for your requirements Many features: delayed or instant work, periodic work, unique work, chained work, many requirements, back-off criteria, extras, easy to test, wake-locks, background threads, … WorkManager
  18. class ImageUploadWorker : Worker() { override fun doWork(): Result {

    val imageId = inputData.getInt(EXTRA_IMAGE, -1) if (imageId <= 0) { return Result.FAILURE } // do something return Result.SUCCESS } } Sample Work
  19. class ImageUploadWorker : Worker() { companion object { const val

    TAG = "ImageUploadWorker" private const val EXTRA_IMAGE = "EXTRA_IMAGE" fun schedule(image: Image) { val request = OneTimeWorkRequestBuilder<ImageUploadWorker>() .addTag(TAG) .setInputData( workDataOf( EXTRA_IMAGE to image.id ) ) .build() WorkManager.getInstance().enqueue(request) } } } Sample Work
  20. Differences android-job WorkManager Exact jobs Daily jobs Transient jobs No

    reflection Custom logger Chained Work Testing Observing changes Own job scheduling engine Async API
  21. class DailyUpdateJob : DailyJob() { override fun onRunDailyJob(params: Params): DailyJobResult

    { // download and show updates return DailyJobResult.SUCCESS } companion object { fun schedule() { if (!JobManager.instance().getAllJobRequestsForTag(TAG).isEmpty()) { return // already scheduled } val builder = JobRequest.Builder(TAG) // add more requirements DailyJob.scheduleAsync( builder, TimeUnit.HOURS.toMillis(5), // between 5am and 8am TimeUnit.HOURS.toMillis(8) ) }}} Daily jobs
  22. -keep class * extends androidx.work.Worker -keep class * extends androidx.work.InputMerger

    # Worker#internalInit is marked as @Keep # We need to keep Data and Extras for the method descriptor of internalInit. -keep class androidx.work.Data -keep class androidx.work.impl.Extras # We reflectively try and instantiate FirebaseJobScheduler when we find a Firebase dependency # on the classpath. -keep class androidx.work.impl.background.firebase.FirebaseJobScheduler Reflection From WorkManager
  23. class App : Application() { override fun onCreate() { super.onCreate()

    JobConfig.addLogger(object : JobLogger { override fun log(priority: Int, tag: String, message: String, t: Throwable?) { // do something } }) JobManager.create(this).addJobCreator(ImageUploadJobCreator) } } Custom Logger
  24. class ImageCompressWorker : Worker() { override fun doWork(): Result {

    val imageId = inputData.getInt(EXTRA_IMAGE, -1) if (imageId <= 0) return Result.FAILURE val newImageId = compressImage(imageId) outputData = workDataOf(EXTRA_IMAGE to newImageId) return Result.SUCCESS } } Chained Work
  25. class ImageUploadWorker : Worker() { override fun doWork(): Result {

    val imageId = inputData.getIntArray(EXTRA_IMAGE) ?: return Result.FAILURE imageId.forEach { uploadImage(it) } return Result.SUCCESS } } Chained Work
  26. fun schedule(images: Array<Image>) { val compressRequests = images.map { image

    -> OneTimeWorkRequestBuilder<ImageCompressWorker>() .setInputData( workDataOf( EXTRA_IMAGE to image.id ) ) .build() } val uploadRequest = OneTimeWorkRequestBuilder<ImageUploadWorker>() .setInputMerger(ArrayCreatingInputMerger::class) .build() WorkManager.getInstance() .beginWith(compressRequests) .then(uploadRequest) .enqueue() } Chained Work
  27. @Test fun `verify worker uploads images`() { val worker =

    ImageUploadWorker() worker.inputData = workDataOf() // doesn't compile :( val result = worker.doWork() assertThat(result).isEqualTo(Worker.Result.SUCCESS) } Testing WorkManager
  28. @Test fun `verify job uploads images`() { val extras =

    PersistableBundleCompat().apply { putInt(ImageUploadJob.EXTRA_IMAGE, 5) } val params = mock<Job.Params> { on { this.extras } doReturn extras } val result = ImageUploadJob().onRunJob(params) // made onRunJob() public assertThat(result).isEqualTo(Job.Result.SUCCESS) } Testing android-job
  29. fun schedule(images: Array<Image>): UUID { // ... val uploadRequest =

    OneTimeWorkRequestBuilder<ImageUploadWorker>() .setInputMerger(ArrayCreatingInputMerger::class.java) .build() WorkManager.getInstance() .beginWith(compressRequests) .then(uploadRequest) .enqueue() return uploadRequest.id } Testing WorkManager Instrumentation Test
  30. @Test fun verifyWorkerUploadsImages() { WorkManagerTestInitHelper.initializeTestWorkManager(InstrumentationRegistry.getTargetContext()) val uuid = ImageUploadWorker.schedule( arrayOf(

    Image(5), Image(6), Image(7) ) ) val status = WorkManager.getInstance().synchronous().getStatusByIdSync(uuid) assertEquals(status?.state, State.SUCCEEDED) } Testing WorkManager Instrumentation Test
  31. val jobId = 5 val request = JobManager.instance().getJobRequest(jobId) // job

    is scheduled val job = JobManager.instance().getJob(jobId) // job is running val result = JobManager.instance().allJobResults[jobId] // job finished Observing changes android-job
  32. val workId = UUID.randomUUID() WorkManager.getInstance().getStatusById(workId).observeForever { status: WorkStatus? -> val

    state = status?.state // ... } val state = WorkManager.getInstance().synchronous().getStatusByIdSync(workId)?.state Observing changes WorkManager
  33. class ImageUploadJob(private val uploadApi: UploadApi) : Job() { public override

    fun onRunJob(params: Params): Result { val imageId = params.extras.getInt(EXTRA_IMAGE, -1) if (imageId <= 0) return Result.FAILURE val error = uploadApi.uploadImage(Image(imageId)).blockingGet() // :( return if (error == null) Result.SUCCESS else Result.FAILURE } } Async API android-job
  34. class AsyncImageUploadWorker : NonBlockingWorker() { private val api: UploadApi =

    DefaultUploadApi private val disposable: Disposable? = null override fun onStartWork(callback: WorkFinishedCallback) { val imageId = inputData.getInt(EXTRA_IMAGE, -1) if (imageId <= 0) { callback.onWorkFinished(Worker.Result.FAILURE) // onWorkFinished is still restricted to the library return } api .uploadImage(Image(imageId)) .subscribe({ callback.onWorkFinished(Worker.Result.SUCCESS) }, { error -> callback.onWorkFinished(Worker.Result.FAILURE) }) } override fun onStopped(cancelled: Boolean) { disposable?.dispose() } } Async API WorkManager
  35. fun schedule(image: Image): UUID { val uploadRequest = OneTimeWorkRequestBuilder<AsyncImageUploadWorker>() //

    expects Worker .setInputData(workDataOf(EXTRA_IMAGE to image.id)) .build() WorkManager.getInstance().enqueue(uploadRequest) return uploadRequest.id } Async API WorkManager
  36. public abstract class Worker extends NonBlockingWorker { // TODO(rahulrav@) Move

    this to a NonBlockingWorker once we are ready to expose it. public enum Result { // .. code } /** * Override this method to do your actual background processing. */ @WorkerThread public abstract @NonNull Result doWork(); /** * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @Override public void onStartWork(@NonNull WorkFinishedCallback callback) { Result result = doWork(); callback.onWorkFinished(result); } } Async API WorkManager
  37. dependencies { implementation 'com.evernote:android-job:1.3.0-alpha06' implementation "android.arch.work:work-runtime:1.0.0-alpha07" } class App :

    Application() { override fun onCreate() { super.onCreate() JobConfig.setApiEnabled(JobApi.WORK_MANAGER, true) // done implicitly JobManager.create(this).addJobCreator(ImageUploadJobCreator) } } From android-job to WorkManager Use both in parallel
  38. Remove android-job from your dependencies Schedule Workers as in a

    new application install Manually copy jobs if necessary From android-job to WorkManager
  39. android-job will be deprecated, what should I do? Can I

    have at least exact jobs? Questions?