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. From AlarmManager to WorkManager Ralf Wondratschek @vRallev

  2. None
  3. http://bit.ly/2w1yLMz

  4. Motivation

  5. Problem To Solve Guaranteed execution of deferrable tasks Job execution

    requirements Batched jobs to save battery
  6. 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
  7. AlarmManager With KitKat the API behavior changed New API for

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

    Platform bugs 2008 2018 2013 2014 2015 2016 2017
  9. Doze

  10. AlarmManager 2008 2018 2013 2014 2015 2016 2017 New method

    for exact alarms in Doze mode
  11. GcmNetworkManager 2008 2018 2013 2014 2015 2016 2017 Similar API

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

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

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

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

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

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

    the short term probably android-job In the long term probably WorkManager
  18. 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
  19. 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
  20. 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
  21. object ImageUploadJobCreator : JobCreator { override fun create(tag: String): Job?

    { return when (tag) { ImageUploadJob.TAG -> ImageUploadJob() SyncJob.TAG -> SyncJob() else -> null } } } Setup
  22. 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
  23. 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
  24. 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
  25. dependencies { implementation "android.arch.work:work-runtime:1.0.0-alpha07" // implementation "android.arch.work:work-runtime-ktx:1.0.0-alpha07" // for Kotlin

    // testImplementation "android.arch.work:work-testing:$workVersion" // for Testing } Setup
  26. 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
  27. 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
  28. 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
  29. fun schedule(): Int { return JobRequest.Builder(TAG) .setExact(TimeUnit.HOURS.toMillis(1)) .build() .schedule() }

    Exact jobs
  30. 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
  31. fun schedule(image: Image) { return JobRequest.Builder(TAG) .startNow() .setTransientExtras(Bundle().apply { putInt(EXTRA_IMAGE,

    image.id) }) .build() .scheduleAsync() } Transient jobs
  32. -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
  33. 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
  34. Chained Work Compress Image Compress Image Compress Image Upload Images

  35. 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
  36. 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
  37. 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
  38. @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
  39. @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
  40. 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
  41. @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
  42. 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
  43. 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
  44. Own Job Scheduling Engine

  45. interface UploadApi { fun uploadImage(image: Image): Completable } Async API

  46. 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
  47. Async API WorkManager NonBlockingWorker Worker ImageUploadWorker

  48. 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
  49. 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
  50. 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
  51. 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
  52. Remove android-job from your dependencies Schedule Workers as in a

    new application install Manually copy jobs if necessary From android-job to WorkManager
  53. Should I replace AsyncTask / Executor / RxJava with WorkManager?

    Questions?
  54. android-job will be deprecated, what should I do? Can I

    have at least exact jobs? Questions?
  55. Questions?

  56. Thank you! Ralf Wondratschek @vRallev