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

Embrace WorkManager

Embrace WorkManager

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.

Pietro F. Maggi

July 03, 2019
Tweet

More Decks by Pietro F. Maggi

Other Decks in Programming

Transcript

  1. .plan • What is WorkManager • Which Worker class is

    right for my job • WorkManager’s status • Cancel Work • Unique Work • Periodic Work • Testing WorkManager • WorkManager Configuration
  2. Jetpack categories Foundation Android KTX AppCompat Auto Benchmark Multidex Security

    Test TV Wear OS by Google Behavior CameraX Download manager Media & playback Notifications Permissions Preferences Sharing Slices UI Animation & transitions Emoji Fragment Layout Palette Architecture Data Binding Lifecycles LiveData Navigation Paging Room ViewModel WorkManager
  3. WorkManager WorkManager is an Android library that runs deferrable background

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

    work when the work's constraints are satisfied. WorkManager is intended for tasks that require a guarantee that the system will run them even if the app exits. Introducing WorkManager: bit.ly/WM_Introduction
  5. Has to finish? I need to run a task NO

    ThreadPool* or Coroutines *If you have system triggers, use a broadcast receiver to get notified
  6. Is Deferrable? Has to finish? I need to run a

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

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

    task YES NO NO ThreadPool* or Coroutines Foreground Service* WorkManager *If you have system triggers, use a broadcast receiver to get notified YES
  9. Compress Upload • Asynchronous one-off and periodic tasks • Chaining

    WorkManager Benefits Filter Image 2 Filter Image 3 Filter Image 1
  10. Compress Upload WorkManager Benefits • Asynchronous one-off and periodic tasks

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

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

    • Chaining with Input/Output • Constraints • Handles compatibility • System health best practices • Guaranteed execution Filter Image 2 Filter Image 3 Filter Image 1 Battery not low Storage not low Has Network
  13. WorkManager Benefits • Asynchronous one-off and periodic tasks • Chaining

    with Input/Output • Constraints • Handles compatibility • System health best practices • Guaranteed execution • Query state to display in UI Uploading...
  14. WorkManager Benefits • Asynchronous one-off and periodic tasks • Chaining

    with Input/Output • Constraints • Handles compatibility • System health best practices • Guaranteed execution • Query state to display in UI Finished
  15. Choose the Background Scheduler API 23+ JobScheduler* YES NO AlarmManager

    and BroadcastReceiver * Available since API level 21, but used only on devices with API level 23+
  16. ListenableWorkers 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 } }
  17. ListenableWorkers 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 } }
  18. ListenableWorkers 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 } } runs on main thread
  19. Worker 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() } } }
  20. Worker 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() } } }
  21. Worker 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() } } } Needs to be Synchronous
  22. A CoroutineWorker sample 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() } } }
  23. A CoroutineWorker sample 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() } } } WorkManager meets Kotlin: bit.ly/WM_Kotlin
  24. RxWorker, for the RxJava2 users class RxDownloadWorker(context : Context, params

    : WorkerParameters) : RxWorker(context, params ) { override fun createWork() : Single<Result> { return Observable.range(0, 100) .flatMap { download("https://www.google.com") } .toList() .map { Result.success() }; } }
  25. WorkManager Dependencies dependencies { def work_version = '2.0.1' implementation "androidx.work:work-runtime:$work_version"

    implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-rxjava2:$work_version" androidTestImplementation "androidx.work:work-testing:$work_version" }
  26. WorkManager Dependencies dependencies { def work_version = '2.0.1' implementation "androidx.work:work-runtime:$work_version"

    implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-rxjava2:$work_version" androidTestImplementation "androidx.work:work-testing:$work_version" }
  27. WorkManager Dependencies dependencies { def work_version = '2.0.1' implementation "androidx.work:work-runtime:$work_version"

    implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-rxjava2:$work_version" androidTestImplementation "androidx.work:work-testing:$work_version" }
  28. WorkManager Dependencies dependencies { def work_version = '2.0.1' implementation "androidx.work:work-runtime:$work_version"

    implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-rxjava2:$work_version" androidTestImplementation "androidx.work:work-testing:$work_version" }
  29. WorkManager Dependencies dependencies { def work_version = '2.0.1' implementation "androidx.work:work-runtime:$work_version"

    implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-rxjava2:$work_version" androidTestImplementation "androidx.work:work-testing:$work_version" }
  30. WorkManager’s WorkRequest - WorkInfo from UUID WorkManager Basics: bit.ly/WM_Basics val

    save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id)
  31. WorkManager’s WorkRequest - WorkInfo from UUID WorkManager Basics: bit.ly/WM_Basics val

    save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkManager.getInstance().getWorkInfoByIdLiveData(save.id)
  32. WorkManager’s WorkRequest - WorkInfo(s) from TAG WorkManager Basics: bit.ly/WM_Basics val

    save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkManager.getInstance().getWorkInfoByIdLiveData(save.id) WorkManager.getInstance().getWorkInfosByTag(TAG_SAVE)
  33. WorkManager.getInstance().getWorkInfosByTagLiveData(TAG_SAVE) WorkManager’s WorkRequest - WorkInfo(s) from TAG WorkManager Basics: bit.ly/WM_Basics

    val save = OneTimeWorkRequestBuilder<SaveImageWorker>() .addTag(TAG_SAVE) .build() WorkManager.getInstance().getWorkInfoById(save.id) WorkManager.getInstance().getWorkInfoByIdLiveData(save.id) WorkManager.getInstance().getWorkInfosByTag(TAG_SAVE)
  34. Worker 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() } } }
  35. Worker 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() } } }
  36. Worker 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() } } }
  37. Worker class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

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

    override fun doWork(): Result { // … } override fun onStopped() { super.onStopped() // Cleanup } }
  39. Unique Work Three different policies for OneTimeWorker: • KEEP •

    REPLACE • APPEND Two different policies for PeriodicWorker: • KEEP • REPLACE • APPEND
  40. Unique Work - Periodic class MyApplication: Application() { override fun

    onCreate() { super.onCreate() val backup = PeriodicWorkRequestBuilder<BackupWorker>(8, TimeUnit.HOURS) .build() WorkManager.getInstance().enqueueUniquePeriodicWork(( "BackupWork", ExistingPeriodicWorkPolicy.KEEP, myWork) } }
  41. Unique Work - Periodic class MyApplication: Application() { override fun

    onCreate() { super.onCreate() val backup = PeriodicWorkRequestBuilder<BackupWorker>(8, TimeUnit.HOURS) .build() WorkManager.getInstance().enqueueUniquePeriodicWork(( "BackupWork", ExistingPeriodicWorkPolicy.KEEP, myWork) } }
  42. Unique Work - Periodic class MyApplication: Application() { override fun

    onCreate() { super.onCreate() val backup = PeriodicWorkRequestBuilder<BackupWorker>(8, TimeUnit.HOURS) .build() WorkManager.getInstance().enqueueUniquePeriodicWork(( "BackupWork", ExistingPeriodicWorkPolicy.KEEP, myWork) } } val workManager = WorkManager.getInstance() workManager.getWorkInfosForUniqueWorkLiveData("BackupWork")
  43. Unique Work - Periodic class MyApplication: Application() { override fun

    onCreate() { super.onCreate() val backup = PeriodicWorkRequestBuilder<BackupWorker>(8, TimeUnit.HOURS) .build() WorkManager.getInstance().enqueueUniquePeriodicWork(( "BackupWork", ExistingPeriodicWorkPolicy.KEEP, myWork) } } val workManager = WorkManager.getInstance() workManager.getWorkInfosForUniqueWorkLiveData("BackupWork")
  44. PeriodicWorkRequest Few differences from OneTimeWorkRequest: • No InitialDelay • InitialDelay

    available in WorkManager v2.1.0-alpha02+ • UniqueWork cannot be APPENDED (only KEEP or REPLACE)
  45. PeriodicWorkRequest Few differences from OneTimeWorkRequest: • No InitialDelay • InitialDelay

    available in WorkManager v2.1.0-alpha02+ • UniqueWork cannot be APPENDED (only KEEP or REPLACE) • There’s no SUCCEEDED state
  46. PeriodicWorkRequest Few differences from OneTimeWorkRequest: • No InitialDelay • InitialDelay

    available in WorkManager v2.1.0-alpha02+ • UniqueWork cannot be APPENDED (only KEEP or REPLACE) • There’s no SUCCEEDED state (and no FAILED state)
  47. RUNNING Life of Periodic Work ENQUEUED CANCELLED RETRY, SUCCESS or

    FAILURE CANCEL WorkManager Periodicity: bit.ly/WM_Periodic
  48. Periodic Work - Initial Delay class DelayPeriodicWorker(ctx: Context, params: WorkerParameters)

    : Worker(ctx, params) { override fun doWork(): Result { // Schedule periodic work val periodicRequest = PeriodicWorkRequestBuilder<MyWorker>( 1, TimeUnit.HOURS ).build() WorkManager.getInstance().enqueue(periodicRequest) return Result.success() } } val delayedWorkRequest = OneTimeWorkRequestBuilder<DelayPeriodicWorker>() .setInitialDelay(6, TimeUnit.HOURS) .build() WorkManager.getInstance().enqueue(delayedWorkRequest)
  49. WorkManager TestInitHelper targetContext = InstrumentationRegistry.getInstrumentation().targetContext WorkManagerTestInitHelper.initializeTestWorkManager(targetContext) targetContext = InstrumentationRegistry.getInstrumentation().targetContext configuration

    = Configuration.Builder() .setMinimumLoggingLevel(Log.DEBUG) .setExecutor(SynchronousExecutor()) .build() WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration) Source: developer.android.com/topic/libraries/architecture/workmanager/how-to/testing
  50. A sample test val inputData = workDataOf(KEY_IMAGE_URI to inputDataUri.toString()) val

    request = OneTimeWorkRequestBuilder<TestWorker>() .setInputData(inputData) .build() workManager.enqueue(request).result.get() val workInfo = workManager.getWorkInfoById(request.id).get() val outputUri = workInfo.outputData.getString(KEY_IMAGE_URI) assertThat(uriFileExists(targetContext, outputUri), `is`(true)) assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED)) Source: developer.android.com/topic/libraries/architecture/workmanager/how-to/testing
  51. TestWorkerBuilder and TestListenableWorkerBuilder (v2.1) @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())) } developer.android.com/topic/libraries/architecture/workmanager/how-to/testing-210
  52. TestWorkerBuilder and TestListenableWorkerBuilder (v2.1) @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())) } WorkManager meets Kotlin: bit.ly/WM_Kotlin
  53. TestWorkerBuilder and TestListenableWorkerBuilder (v2.1) @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())) } WorkManager meets Kotlin: bit.ly/WM_Kotlin
  54. TestWorkerBuilder and TestListenableWorkerBuilder (v2.1) @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())) } WorkManager meets Kotlin: bit.ly/WM_Kotlin
  55. Why? • Provide your custom executor • Setup a different

    logging level • Setup a custom WorkerFactory
  56. Why? • Provide your custom executor • Setup a different

    logging level • Setup a custom WorkerFactory (for DI)
  57. WorkManager configuration - Make a new configuration class MyApplication: Application()

    { override fun onCreate() { super.onCreate() // provide custom configuration val config = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory()) .build() //initialize WorkManager WorkManager.initialize(this, config) val workManager = WorkManager.getInstance() } }
  58. WorkManager configuration - Make a new configuration class MyApplication: Application()

    { override fun onCreate() { super.onCreate() // provide custom configuration val config = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory()) .build() //initialize WorkManager WorkManager.initialize(this, config) val workManager = WorkManager.getInstance() } }
  59. WorkManager configuration - Make a new configuration class MyApplication: Application()

    { override fun onCreate() { super.onCreate() // provide custom configuration val config = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory()) .build() //initialize WorkManager WorkManager.initialize(this, config) val workManager = WorkManager.getInstance() } }
  60. WorkManager configuration - Make a new configuration class MyApplication: Application()

    { override fun onCreate() { super.onCreate() // provide custom configuration val config = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory()) .build() //initialize WorkManager WorkManager.initialize(this, config) val workManager = WorkManager.getInstance() } }
  61. WorkManager configuration - Build your factory class MyWorkerFactory(private val appContext:

    Context) : WorkerFactory() { override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker { if (workerClassName.equals("com.sample.myWorker")) { return UpvoteStoryWorker(appContext, workerParameters) } else { throw Exception("Unexpected Worker classname: $workerClassName") } } }
  62. WorkManager configuration - New 2.1.x on demand initialization class MyApplication

    : Application(), Configuration.Provider { override fun getWorkManagerConfiguration(): Configuration = // provide custom configuration Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory(this)) .build() }
  63. WorkManager configuration - New 2.1.x on demand initialization class MyApplication

    : Application(), Configuration.Provider { override fun getWorkManagerConfiguration(): Configuration = // provide custom configuration Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory(this)) .build() }
  64. WorkManager configuration - New 2.1.x on demand initialization class MyApplication

    : Application(), Configuration.Provider { override fun getWorkManagerConfiguration(): Configuration = // provide custom configuration Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(MyWorkerFactory(this)) .build() } WorkManager.getInstance(context)
  65. WorkManager in Plaid class UpvoteStoryWorker(appContext: Context, workerParams: WorkerParameters, private val

    service: DesignerNewsService) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { return try { val storyId = inputData.getLong(KEY_STORY_ID, 0) val userId = inputData.getLong(KEY_USER_ID, 0) val request = UpvoteStoryRequest(storyId, userId) val response = service.upvoteStoryV2(request).await() if (response.isSuccessful) { Result.success() } else { Result.failure() } } catch (e: Exception) { Result.failure() } } }
  66. WorkManager in Plaid class UpvoteStoryWorkerFactory(private val service: DesignerNewsService) : WorkerFactory()

    { override fun createWorker( appContext: Context, workerClassName: String, workerParameters: WorkerParameters ): ListenableWorker { if (workerClassName.equals("io.plaidapp.designernews.worker.UpvoteStoryWorker")) { return UpvoteStoryWorker(appContext, workerParameters, service) } else { throw Exception("Unexpected Worker classname: $workerClassName") } } }
  67. DelegatingWorkerFactory (v2.1) A WorkerFactory which delegates to other factories. Factories

    can register themselves as delegates, and they will be invoked in order until a delegated factory returns a non-null ListenableWorker instance. Source: developer.android.com/reference/androidx/work/DelegatingWorkerFactory
  68. @Provides @FeatureScope fun provideWorkManager(service: DesignerNewsService): WorkManager { // provide custom

    configuration val appContext = context.applicationContext if (appContext is Configuration.Provider) { val factory = appContext.getWorkManagerConfiguration().workerFactory as PlaidWorkerFactory factory.addFactory(UpvoteStoryWorkerFactory(service)) } return WorkManager.getInstance(context) } DelegatingWorkerFactory (v2.1)
  69. Things to keep in mind - Register your WorkerFactory -

    Renaming a Worker requires to cancel their work requests - WorkManager and on demand Delivery...
  70. • New developments on v2.x branch • Fixes backported to

    v1.x branch • Use Kotlin? CoroutineWorker • Don’t forget to handle cancellation and stopping • If you need to do something once, make it unique • WorkRequest are persisted in a DB (including Data) Few things before I take off..
  71. Resources Documentation: developer.android.com/topic/libraries/architecture/workmanager/ Codelab: codelabs.developers.google.com/codelabs/android-workmanager-kt/ Release Notes: developer.android.com/jetpack/androidx/releases/work Working with

    WorkManager talk at ADS: www.youtube.com/watch?v=83a4rYXsDs0 Guide to background processing: developer.android.com/guide/background/ WorkManager Source Code (part of AOSP): android.googlesource.com/platform/frameworks/support/+/master/work
  72. Resources WorkManager Blog Series: • Introducing WorkManager bit.ly/WM_Introduction • WorkManager

    Basics bit.ly/WM_Basics • WorkManager meets Kotlin bit.ly/WM_Kotlin • WorkManager Periodicity bit.ly/WM_Periodic