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

Pietro F. Maggi

November 16, 2019
Tweet

More Decks by Pietro F. Maggi

Other Decks in Technology

Transcript

  1. WorkManager is an Android library that runs deferrable background work

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

    when the constraints are satisfied. WorkManager
  3. 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
  4. 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
  5. • Asynchronous one-off and periodic tasks • Chaining with Input/Output

    WorkManager Benefits Upload Compress Filter Image 2 Filter Image 3 Filter Image 1
  6. • 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
  7. • 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. JobScheduler Store in WorkManager Database GcmNetworkManager How does WorkManager persist

    work? API 23+ Has Google Play and work-gcm? AlarmManager Yes No API 22-
  30. JobScheduler GcmNetworkManager AlarmManager GreedyScheduler Start app process (if needed) Ask

    WorkManager to Run Work How does WorkManager run your Work?
  31. 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)
  32. 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)
  33. Life of a chain of work If a unit of

    work fails, all dependent work is marked as FAILED
  34. 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
  35. 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
  36. 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
  37. 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
  38. class MyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override

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

    fun doWork(): Result { // … } override fun onStopped() { super.onStopped() // Cleanup } } Handling Stoppages
  40. @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)
  41. @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
  42. @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
  43. @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!
  44. @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)
  45. @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
  46. @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
  47. @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
  48. 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
  49. 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