Slide 1

Slide 1 text

From AlarmManager to WorkManager Ralf Wondratschek @vRallev

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

http://bit.ly/2w1yLMz

Slide 4

Slide 4 text

Motivation

Slide 5

Slide 5 text

Problem To Solve Guaranteed execution of deferrable tasks Job execution requirements Batched jobs to save battery

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

AlarmManager With KitKat the API behavior changed New API for scheduling exact jobs 2008 2018 2013 2014 2015 2016 2017

Slide 8

Slide 8 text

JobScheduler Fluent API Respects device state Only on API 21+ Platform bugs 2008 2018 2013 2014 2015 2016 2017

Slide 9

Slide 9 text

Doze

Slide 10

Slide 10 text

AlarmManager 2008 2018 2013 2014 2015 2016 2017 New method for exact alarms in Doze mode

Slide 11

Slide 11 text

GcmNetworkManager 2008 2018 2013 2014 2015 2016 2017 Similar API like JobScheduler API 9+ Part of the Play Services SDK

Slide 12

Slide 12 text

android-job 2008 2018 2013 2014 2015 2016 2017 Single API to use the AlarmManager, JobScheduler and GcmNetworkManager Many features backported

Slide 13

Slide 13 text

Firebase JobDispatcher 2008 2018 2013 2014 2015 2016 2017 Same API as JobScheduler Wrapper for job schedule engines, but only supports the GcmNetworkManager

Slide 14

Slide 14 text

Android Oreo 2008 2018 2013 2014 2015 2016 2017 Background service limitations Broadcast removal

Slide 15

Slide 15 text

JobIntentService 2008 2018 2013 2014 2015 2016 2017 Replacement for IntentService Respects app state Only for running jobs immediately

Slide 16

Slide 16 text

WorkManager 2008 2018 2013 2014 2015 2016 2017 Single API to use the AlarmManager, JobScheduler or JobDispatcher Architecture component and part of Android Jetpack

Slide 17

Slide 17 text

Which API or component should you use? It depends In the short term probably android-job In the long term probably WorkManager

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

object ImageUploadJobCreator : JobCreator { override fun create(tag: String): Job? { return when (tag) { ImageUploadJob.TAG -> ImageUploadJob() SyncJob.TAG -> SyncJob() else -> null } } } Setup

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

class ImageUploadWorker : Worker() { companion object { const val TAG = "ImageUploadWorker" private const val EXTRA_IMAGE = "EXTRA_IMAGE" fun schedule(image: Image) { val request = OneTimeWorkRequestBuilder() .addTag(TAG) .setInputData( workDataOf( EXTRA_IMAGE to image.id ) ) .build() WorkManager.getInstance().enqueue(request) } } } Sample Work

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

fun schedule(): Int { return JobRequest.Builder(TAG) .setExact(TimeUnit.HOURS.toMillis(1)) .build() .schedule() } Exact jobs

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

fun schedule(image: Image) { return JobRequest.Builder(TAG) .startNow() .setTransientExtras(Bundle().apply { putInt(EXTRA_IMAGE, image.id) }) .build() .scheduleAsync() } Transient jobs

Slide 32

Slide 32 text

-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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Chained Work Compress Image Compress Image Compress Image Upload Images

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

fun schedule(images: Array) { val compressRequests = images.map { image -> OneTimeWorkRequestBuilder() .setInputData( workDataOf( EXTRA_IMAGE to image.id ) ) .build() } val uploadRequest = OneTimeWorkRequestBuilder() .setInputMerger(ArrayCreatingInputMerger::class) .build() WorkManager.getInstance() .beginWith(compressRequests) .then(uploadRequest) .enqueue() } Chained Work

Slide 38

Slide 38 text

@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

Slide 39

Slide 39 text

@Test fun `verify job uploads images`() { val extras = PersistableBundleCompat().apply { putInt(ImageUploadJob.EXTRA_IMAGE, 5) } val params = mock { on { this.extras } doReturn extras } val result = ImageUploadJob().onRunJob(params) // made onRunJob() public assertThat(result).isEqualTo(Job.Result.SUCCESS) } Testing android-job

Slide 40

Slide 40 text

fun schedule(images: Array): UUID { // ... val uploadRequest = OneTimeWorkRequestBuilder() .setInputMerger(ArrayCreatingInputMerger::class.java) .build() WorkManager.getInstance() .beginWith(compressRequests) .then(uploadRequest) .enqueue() return uploadRequest.id } Testing WorkManager Instrumentation Test

Slide 41

Slide 41 text

@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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Own Job Scheduling Engine

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Async API WorkManager NonBlockingWorker Worker ImageUploadWorker

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

fun schedule(image: Image): UUID { val uploadRequest = OneTimeWorkRequestBuilder() // expects Worker .setInputData(workDataOf(EXTRA_IMAGE to image.id)) .build() WorkManager.getInstance().enqueue(uploadRequest) return uploadRequest.id } Async API WorkManager

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Remove android-job from your dependencies Schedule Workers as in a new application install Manually copy jobs if necessary From android-job to WorkManager

Slide 53

Slide 53 text

Should I replace AsyncTask / Executor / RxJava with WorkManager? Questions?

Slide 54

Slide 54 text

android-job will be deprecated, what should I do? Can I have at least exact jobs? Questions?

Slide 55

Slide 55 text

Questions?

Slide 56

Slide 56 text

Thank you! Ralf Wondratschek @vRallev