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. View Slide

  2. Embracing WorkManager
    Pietro Maggi | Android DevRel Engineer @ Google
    bit.ly/pfm-wm-devfest19

    View Slide

  3. The basics
    ..or what is WorkManager

    View Slide

  4. Part of Android Jetpack
    Accelerate
    development
    Eliminate
    boilerplate code
    Build high quality,
    robust apps

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  9. ● Asynchronous one-off and periodic tasks
    WorkManager Benefits
    Compress

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  13. I need to run a task

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  20. Release History
    1.0 / 2.0
    Initial release in support lib (1.0) / AndroidX (2.0)
    1.0 / 2.0

    View Slide

  21. Release History
    1.0 / 2.0
    On-demand initialization, new testing support
    2.1
    2.1

    View Slide

  22. Release History Present
    GcmNetworkManager support (API 22 and below)
    2.1 2.2
    1.0 / 2.0

    View Slide

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

    View Slide

  24. WorkManager’s
    Worker classes

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  41. How does
    WorkManager
    interact with
    Android?

    View Slide

  42. class UploadWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    override fun doWork(): Result {
    uploadImages()
    return Result.success()
    }
    }
    val uploadWorkRequest = OneTimeWorkRequestBuilder()
    .setConstraints(constraints)
    .build()
    WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)
    Enqueue a WorkRequest

    View Slide

  43. Store in WorkManager Database
    How does WorkManager persist work?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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-

    View Slide

  48. JobScheduler
    GcmNetworkManager
    How does WorkManager run your Work?
    AlarmManager
    GreedyScheduler
    Start app process
    (if needed)
    Ask WorkManager
    to Run Work

    View Slide

  49. JobScheduler
    GcmNetworkManager
    AlarmManager
    GreedyScheduler
    Start app process
    (if needed)
    Ask WorkManager
    to Run Work
    How does WorkManager run your Work?

    View Slide

  50. Observe Work

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  54. val save = OneTimeWorkRequestBuilder()
    .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)

    View Slide

  55. val save = OneTimeWorkRequestBuilder()
    .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)

    View Slide

  56. BLOCKED
    Life of OneTimeWorkRequest

    View Slide

  57. BLOCKED ENQUEUED
    Life of OneTimeWorkRequest

    View Slide

  58. BLOCKED ENQUEUED
    Life of OneTimeWorkRequest
    RUNNING

    View Slide

  59. BLOCKED ENQUEUED
    Life of OneTimeWorkRequest
    RUNNING SUCCEEDED
    SUCCESS

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  63. ENQUEUED
    Life of PeriodicWorkRequest

    View Slide

  64. ENQUEUED
    Life of PeriodicWorkRequest
    RUNNING

    View Slide

  65. ENQUEUED
    Life of PeriodicWorkRequest
    RUNNING
    SUCCESS

    View Slide

  66. SUCCESS, RETRY
    ENQUEUED
    Life of PeriodicWorkRequest
    RUNNING
    SUCCESS

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  71. Cancel Work and
    Handling Stoppages

    View Slide

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

    View Slide

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

    View Slide

  74. val save = OneTimeWorkRequestBuilder()
    .addTag(TAG_SAVE)
    .build()
    WorkManager.getInstance().cancelWorkById(save.id)
    Cancel Work from TAG
    WorkManager.getInstance().cancelAllWorkByTag(TAG_SAVE)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  80. Testing

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  86. @Test
    fun testMyWorkRetry() {
    val data = workDataOf("SERVER_URL" to "http://fake.url")
    // Get the ListenableWorker with a RunAttemptCount of 2
    val worker = TestListenableWorkerBuilder(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

    View Slide

  87. @Test
    fun testMyWorkRetry() {
    val data = workDataOf("SERVER_URL" to "http://fake.url")
    // Get the ListenableWorker with a RunAttemptCount of 2
    val worker = TestListenableWorkerBuilder(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

    View Slide

  88. @Test
    fun testMyWorkRetry() {
    val data = workDataOf("SERVER_URL" to "http://fake.url")
    // Get the ListenableWorker with a RunAttemptCount of 2
    val worker = TestListenableWorkerBuilder(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

    View Slide

  89. Where to go…
    ... from here?

    View Slide

  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

    View Slide

  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

    View Slide

  92. THANKS!
    Pietro Maggi, @pfmaggi
    bit.ly/pfm-wm-devfest19

    View Slide