$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  2. View Slide

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

    View Slide

  4. Motivation

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  9. Doze

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  27. 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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  34. Chained Work
    Compress Image Compress Image Compress Image
    Upload Images

    View Slide

  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

    View Slide

  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

    View Slide

  37. 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

    View Slide

  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

    View Slide

  39. @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

    View Slide

  40. 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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  44. Own Job Scheduling Engine

    View Slide

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

    View Slide

  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

    View Slide

  47. Async API WorkManager
    NonBlockingWorker
    Worker
    ImageUploadWorker

    View Slide

  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

    View Slide

  49. 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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  55. Questions?

    View Slide

  56. Thank you!
    Ralf Wondratschek
    @vRallev

    View Slide