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

Embracing WorkManager - DevFest19 edition

Embracing WorkManager - DevFest19 edition

After all of 2018 spent as alpha and beta, WorkManager graduated in 2019 to its first stable release and it is now the recommended solution for scheduling and executing deferrable background tasks in Android.
In this talk we’ll quickly look into which use cases are a good fit for WorkManager, and which are better handled by different tools/APIs. Then we will jump into the API itself covering the different options available, from the simple Worker class to the newer CoroutineWorker.
All this with an eye on how you can (and should) test your Workers and how you can implement some of the few missing features that are not yet covered by WorkManager out of the box.
With this talk you’ll learn:
When to use (and when not) WorkManager
Which Worker class to use for your particular use case
How to test your Worker classes

3dfa913cbcfe896af79e0484084c316b?s=128

Pietro F. Maggi

November 16, 2019
Tweet

Transcript

  1. None
  2. Embracing WorkManager Pietro Maggi | Android DevRel Engineer @ Google

    bit.ly/pfm-wm-devfest19
  3. The basics ..or what is WorkManager

  4. Part of Android Jetpack Accelerate development Eliminate boilerplate code Build

    high quality, robust apps
  5. WorkManager is an Android library that runs deferrable background work

    when the constraints are satisfied. WorkManager
  6. WorkManager is an Android library that runs deferrable background work

    when the constraints are satisfied. WorkManager
  7. WorkManager is an Android library that runs deferrable background work

    when the constraints are satisfied. WorkManager is intended for tasks that require a guarantee that the system will run them even if the app exits. WorkManager
  8. WorkManager is an Android library that runs deferrable background work

    when the constraints are satisfied. WorkManager is intended for tasks that require a guarantee that the system will run them even if the app exits. WorkManager
  9. • Asynchronous one-off and periodic tasks WorkManager Benefits Compress

  10. • Asynchronous one-off and periodic tasks • Chaining with Input/Output

    WorkManager Benefits Upload Compress Filter Image 2 Filter Image 3 Filter Image 1
  11. • Asynchronous one-off and periodic tasks • Chaining with Input/Output

    • Constraints WorkManager Benefits Upload Compress Filter Image 2 Filter Image 3 Filter Image 1 Battery not low Storage not low Has Network
  12. • Asynchronous one-off and periodic tasks • Chaining with Input/Output

    • Constraints • Handles compatibility • System health best practices • Guaranteed execution WorkManager Benefits Upload Compress Filter Image 2 Filter Image 3 Filter Image 1 Battery not low Storage not low Has Network
  13. I need to run a task

  14. Has to finish? I need to run a task

  15. Executor* or Coroutines Has to finish? I need to run

    a task NO *If you have system triggers, use a broadcast receiver to get notified
  16. Executor* or Coroutines Is Deferrable? Has to finish? I need

    to run a task YES NO *If you have system triggers, use a broadcast receiver to get notified
  17. NO Executor* or Coroutines Is Deferrable? Has to finish? I

    need to run a task YES NO Foreground Service* *If you have system triggers, use a broadcast receiver to get notified
  18. NO Executor* or Coroutines WorkManager Is Deferrable? Has to finish?

    I need to run a task YES NO Foreground Service* *If you have system triggers, use a broadcast receiver to get notified YES
  19. NO Executor* or Coroutines WorkManager Is Deferrable? Has to finish?

    I need to run a task YES NO Foreground Service* *If you have system triggers, use a broadcast receiver to get notified YES v 2.3-alpha02
  20. Release History 1.0 / 2.0 Initial release in support lib

    (1.0) / AndroidX (2.0) 1.0 / 2.0
  21. Release History 1.0 / 2.0 On-demand initialization, new testing support

    2.1 2.1
  22. Release History Present GcmNetworkManager support (API 22 and below) 2.1

    2.2 1.0 / 2.0
  23. Release History Future Progress and setForeground APIs 2.1 2.3 1.0

    / 2.0 2.2
  24. WorkManager’s Worker classes

  25. ListenableWorker Worker Meet the Worker Family! RxWorker CoroutineWorker* *Only available

    using Kotlin
  26. class LocationWorker(context : Context, params : WorkerParameters) : ListenableWorker(context, params)

    { val future = ResolvableFuture.create<Result>() override fun startWork(): ListenableFuture<Result> { if (hasPermissions()) { getLocation() } else { future.set(Result.failure()) } return future } } ListenableWorker
  27. class LocationWorker(context : Context, params : WorkerParameters) : ListenableWorker(context, params)

    { val future = ResolvableFuture.create<Result>() override fun startWork(): ListenableFuture<Result> { if (hasPermissions()) { getLocation() } else { future.set(Result.failure()) } return future } } ListenableWorker
  28. class LocationWorker(context : Context, params : WorkerParameters) : ListenableWorker(context, params)

    { val future = ResolvableFuture.create<Result>() override fun startWork(): ListenableFuture<Result> { if (hasPermissions()) { getLocation() } else { future.set(Result.failure()) } return future } } ListenableWorker Runs on main thread
  29. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { val in = inputData.getString(KEY_STRING) return try { // Do something that can fail Result.success(workDataOf(KEY_INT to 42)) } catch (throwable: Throwable) { // For errors, return FAILURE or RETRY Result.failure() } } } Worker
  30. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { val in = inputData.getString(KEY_STRING) return try { // Do something that can fail Result.success(workDataOf(KEY_INT to 42)) } catch (throwable: Throwable) { // For errors, return FAILURE or RETRY Result.failure() } } } Worker
  31. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { val in = inputData.getString(KEY_STRING) return try { // Do something that can fail Result.success(workDataOf(KEY_INT to 42)) } catch (throwable: Throwable) { // For errors, return FAILURE or RETRY Result.failure() } } } Worker Needs to be Synchronous
  32. class RefreshMainDataWork(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override

    suspend fun doWork(): Result { val database = getDatabase(applicationContext) val repository = TitleRepository(MainNetworkImpl, database.titleDao) return try { repository.refreshTitle() Result.success() } catch (error: TitleRefreshError) { Result.failure() } } } CoroutineWorker
  33. class RefreshMainDataWork(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override

    suspend fun doWork(): Result { val database = getDatabase(applicationContext) val repository = TitleRepository(MainNetworkImpl, database.titleDao) return try { repository.refreshTitle() Result.success() } catch (error: TitleRefreshError) { Result.failure() } } } CoroutineWorker
  34. class RefreshMainDataWork(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override

    suspend fun doWork(): Result { val database = getDatabase(applicationContext) val repository = TitleRepository(MainNetworkImpl, database.titleDao) return try { repository.refreshTitle() Result.success() } catch (error: TitleRefreshError) { Result.failure() } } } CoroutineWorker
  35. dependencies { def work_version = "2.2.0" // (Java only) implementation

    "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" // optional - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" } WorkManager Dependencies
  36. dependencies { def work_version = "2.2.0" // (Java only) implementation

    "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" // optional - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" } WorkManager Dependencies
  37. dependencies { def work_version = "2.2.0" // (Java only) implementation

    "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" // optional - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" } WorkManager Dependencies
  38. dependencies { def work_version = "2.2.0" // (Java only) implementation

    "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" // optional - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" } WorkManager Dependencies
  39. dependencies { def work_version = "2.2.0" // (Java only) implementation

    "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" // optional - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" } WorkManager Dependencies
  40. dependencies { def work_version = "2.2.0" // optional (but recommended)

    - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" // (Java only) implementation "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" } WorkManager Dependencies
  41. How does WorkManager interact with Android?

  42. class UploadWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override

    fun doWork(): Result { uploadImages() return Result.success() } } val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>() .setConstraints(constraints) .build() WorkManager.getInstance(myContext).enqueue(uploadWorkRequest) Enqueue a WorkRequest
  43. Store in WorkManager Database How does WorkManager persist work?

  44. JobScheduler Store in WorkManager Database How does WorkManager persist work?

    API 23+
  45. JobScheduler Store in WorkManager Database How does WorkManager persist work?

    API 23+ Has Google Play and work-gcm? API 22-
  46. JobScheduler Store in WorkManager Database GcmNetworkManager How does WorkManager persist

    work? API 23+ Has Google Play and work-gcm? Yes API 22-
  47. JobScheduler Store in WorkManager Database GcmNetworkManager How does WorkManager persist

    work? API 23+ Has Google Play and work-gcm? AlarmManager Yes No API 22-
  48. JobScheduler GcmNetworkManager How does WorkManager run your Work? AlarmManager GreedyScheduler

    Start app process (if needed) Ask WorkManager to Run Work
  49. JobScheduler GcmNetworkManager AlarmManager GreedyScheduler Start app process (if needed) Ask

    WorkManager to Run Work How does WorkManager run your Work?
  50. Observe Work

  51. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager’s WorkRequest

  52. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkInfo from UUID

    WorkManager.getInstance().getWorkInfoByIdLiveData(save.id)
  53. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkInfo from UUID

    WorkManager.getInstance().getWorkInfoByIdLiveData(save.id)
  54. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkInfo(s) from TAG

    WorkManager.getInstance().getWorkInfoByIdLiveData(save.id) WorkManager.getInstance().getWorkInfosByTag(TAG_SAVE) WorkManager.getInstance().getWorkInfosByTagLiveData(TAG_SAVE)
  55. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkInfo(s) from TAG

    WorkManager.getInstance().getWorkInfoByIdLiveData(save.id) WorkManager.getInstance().getWorkInfosByTag(TAG_SAVE) WorkManager.getInstance().getWorkInfosByTagLiveData(TAG_SAVE)
  56. BLOCKED Life of OneTimeWorkRequest

  57. BLOCKED ENQUEUED Life of OneTimeWorkRequest

  58. BLOCKED ENQUEUED Life of OneTimeWorkRequest RUNNING

  59. BLOCKED ENQUEUED Life of OneTimeWorkRequest RUNNING SUCCEEDED SUCCESS

  60. BLOCKED ENQUEUED Life of OneTimeWorkRequest RUNNING SUCCEEDED CANCELLED SUCCESS RETRY

  61. BLOCKED ENQUEUED Life of OneTimeWorkRequest RUNNING SUCCEEDED FAILED CANCELLED SUCCESS

    FAILURE RETRY
  62. BLOCKED ENQUEUED Life of OneTimeWorkRequest RUNNING SUCCEEDED FAILED CANCELLED CANCEL

    SUCCESS FAILURE RETRY
  63. ENQUEUED Life of PeriodicWorkRequest

  64. ENQUEUED Life of PeriodicWorkRequest RUNNING

  65. ENQUEUED Life of PeriodicWorkRequest RUNNING SUCCESS

  66. SUCCESS, RETRY ENQUEUED Life of PeriodicWorkRequest RUNNING SUCCESS

  67. SUCCESS, RETRY SUCCESS, RETRY, FAILURE ENQUEUED Life of PeriodicWorkRequest RUNNING

    SUCCESS
  68. SUCCESS, RETRY SUCCESS, RETRY, FAILURE ENQUEUED Life of PeriodicWorkRequest RUNNING

    CANCELLED CANCEL SUCCESS
  69. Life of a chain of work If a unit of

    work fails, all dependent work is marked as FAILED
  70. Life of a chain of work If a unit of

    work fails, all dependent work is marked as FAILED If a unit of work is cancelled, all dependent work is marked as CANCELLED
  71. Cancel Work and Handling Stoppages

  72. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager’s WorkRequest

  73. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().cancelWorkById(save.id) Cancel Work from

    UUID
  74. val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().cancelWorkById(save.id) Cancel Work from

    TAG WorkManager.getInstance().cancelAllWorkByTag(TAG_SAVE)
  75. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { val in = inputData.getString(KEY_STRING) return try { // Do something that can fail if (isStopped()) return Result.failure() Result.success(workDataOf(KEY_INT to 42)) } catch (throwable: Throwable) { // For errors, return FAILURE or RETRY Result.failure() } } } Handle Stopping Work
  76. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { val in = inputData.getString(KEY_STRING) return try { // Do something that can fail if (isStopped()) return Result.failure() Result.success(workDataOf(KEY_INT to 42)) } catch (throwable: Throwable) { // For errors, return FAILURE or RETRY Result.failure() } } } Handling Stoppages
  77. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { val in = inputData.getString(KEY_STRING) return try { // Do something that can fail if (isStopped()) return Result.success() // this is not used Result.success(workDataOf(KEY_INT to 42)) } catch (throwable: Throwable) { // For errors, return FAILURE or RETRY Result.failure() } } } Handling Stoppages
  78. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { // … } override fun onStopped() { super.onStopped() // Cleanup } } Handling Stoppages
  79. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

    fun doWork(): Result { // … } override fun onStopped() { super.onStopped() // Cleanup } } Handling Stoppages
  80. Testing

  81. @Before fun setup() { context = ApplicationProvider.getApplicationContext() workManager = WorkManager.getInstance(context)

    } @Test fun testRefreshMainDataWork() { val worker = TestListenableWorkerBuilder<TestWorker>(context).build() val result = worker.startWork().get() assertThat(result, `is` (Result.success())) } TestListenableWorkerBuilder (v2.1)
  82. @Before fun setup() { context = ApplicationProvider.getApplicationContext() workManager = WorkManager.getInstance(context)

    } @Test fun testRefreshMainDataWork() { val worker = TestListenableWorkerBuilder<TestWorker>(context).build() val result = worker.startWork().get() assertThat(result, `is` (Result.success())) } Build your Worker
  83. @Before fun setup() { context = ApplicationProvider.getApplicationContext() workManager = WorkManager.getInstance(context)

    } @Test fun testRefreshMainDataWork() { val worker = TestListenableWorkerBuilder<TestWorker>(context).build() val result = worker.startWork().get() assertThat(result, `is` (Result.success())) } Directly invoke your Work
  84. @Before fun setup() { context = ApplicationProvider.getApplicationContext() workManager = WorkManager.getInstance(context)

    } @Test fun testRefreshMainDataWork() { val worker = TestListenableWorkerBuilder<TestWorker>(context).build() val result = worker.startWork().get() assertThat(result, `is` (Result.success())) } Test!
  85. @Test fun testMyWorkRetry() { val data = workDataOf("SERVER_URL" to "http://fake.url")

    // Get the ListenableWorker with a RunAttemptCount of 2 val worker = TestListenableWorkerBuilder<MyWork>(context) .setInputData(data) .setRunAttemptCount(2) .build() // Start the work synchronously val result = worker.startWork().get() assertThat(result, `is`(Result.retry())) } TestListenableWorkerBuilder (v2.1)
  86. @Test fun testMyWorkRetry() { val data = workDataOf("SERVER_URL" to "http://fake.url")

    // Get the ListenableWorker with a RunAttemptCount of 2 val worker = TestListenableWorkerBuilder<MyWork>(context) .setInputData(data) .setRunAttemptCount(2) .build() // Start the work synchronously val result = worker.startWork().get() assertThat(result, `is`(Result.retry())) } Test Work with input data
  87. @Test fun testMyWorkRetry() { val data = workDataOf("SERVER_URL" to "http://fake.url")

    // Get the ListenableWorker with a RunAttemptCount of 2 val worker = TestListenableWorkerBuilder<MyWork>(context) .setInputData(data) .setRunAttemptCount(2) .build() // Start the work synchronously val result = worker.startWork().get() assertThat(result, `is`(Result.retry())) } Test Work for Retry behaviour
  88. @Test fun testMyWorkRetry() { val data = workDataOf("SERVER_URL" to "http://fake.url")

    // Get the ListenableWorker with a RunAttemptCount of 2 val worker = TestListenableWorkerBuilder<MyWork>(context) .setInputData(data) .setRunAttemptCount(2) .build() // Start the work synchronously val result = worker.startWork().get() assertThat(result, `is`(Result.retry())) } Test Work for Retry behaviour
  89. Where to go… ... from here?

  90. Resources: Just Starting? --> Codelab: codelabs.developers.google.com/codelabs/android-workmanager-kt/ Want to deep dive?

    --> Documentation: d.android.com/topic/libraries/architecture/workmanager/ I know what I’m doing! --> Release Notes: d.android.com/jetpack/androidx/releases/work bit.ly/pfm-wm-devfest19
  91. More Resources: Videos: - ADS2018 - Working with WorkManager -

    ADS2019 - WorkManager: Beyond the Basics Medium Blogs: - Introducing WorkManager - WorkManager Basics - WorkManager Periodicity - WorkManager meets Kotlin - More coming... bit.ly/pfm-wm-devfest19
  92. THANKS! Pietro Maggi, @pfmaggi bit.ly/pfm-wm-devfest19