1 of 31

Working with WorkManager

The Architecture Component for deferrable, asynchronous background work ⚙️

By Nick Rout

Android Engineer at Over

Twitter: @ricknout

2 of 31

What is it ❓

  • A Jetpack Architecture Component!

  • For (potentially long running) async background work

  • Deferrable: not required to run immediately
  • Constrained: runs when device constraints are met
  • Guaranteed: runs even if app exits / device restarts

3 of 31

What is it ❓

  • Wraps existing frameworks, depending on device API level
    • API 23+: JobScheduler
    • API 14-22: BroadcastReceiver + AlarmManager

  • Works with or without Google Play Services

  • Plays nicely with (and internally makes use of) other Architecture Components eg. LiveData, Room

4 of 31

When should it be used 🤔

  • “Fire and forget” scenarios
  • Syncing local data (periodically) with network
  • Sending analytics events
  • Large file uploads/downloads
  • Applying filters to a Bitmap and saving to storage
  • A chain of all of the above

5 of 31

When should it be used 🤔

6 of 31

Rugby Ranker 🏉

  • WorkManager used to periodically sync World Rugby rankings/matches
  • Work involves network (Retrofit), database (Room), SharedPreferences

7 of 31

Basic setup 🏁

  • Latest version: 1.0.0-rc02

  • In this presentation: Kotlin (hence -ktx)

  • Gradle dependencies:

implementation “android.arch.work:work-runtime-ktx:$work-version”

implementation “android.arch.work:work-testing:$work-version”

8 of 31

Your first Worker 🥇

  • Extend the Worker class
  • Override the doWork() function
  • Return a Result
    • Success, Retry, Failure
    • Optional: output Data (Map<String, ?>)

9 of 31

Your first Worker 🥇

class RankingsWorker(

context: Context,

workerParams: WorkerParameters

private val rankingsRepository: RankingsRepository

) : Worker(context, workerParams) {

override fun doWork() = fetchAndCacheLatestWorldRugbyRankings()

private fun fetchAndCacheLatestWorldRugbyRankings(): Result {

val success = rankingsRepository

.fetchAndCacheLatestWorldRugbyRankingsSync()

return if (success) Result.success() else Result.retry()

}

}

10 of 31

Your first Worker 🥇

class RankingsWorker(

context: Context,

workerParams: WorkerParameters

private val rankingsRepository: RankingsRepository

) : Worker(context, workerParams) {

override fun doWork() = fetchAndCacheLatestWorldRugbyRankings()

private fun fetchAndCacheLatestWorldRugbyRankings(): Result {

val success = rankingsRepository

.fetchAndCacheLatestWorldRugbyRankingsSync()

return if (success) Result.success() else Result.retry()

}

}

11 of 31

Your first Worker 🥇

class RankingsWorker(

context: Context,

workerParams: WorkerParameters

private val rankingsRepository: RankingsRepository

) : Worker(context, workerParams) {

override fun doWork() = fetchAndCacheLatestWorldRugbyRankings()

private fun fetchAndCacheLatestWorldRugbyRankings(): Result {

val success = rankingsRepository

.fetchAndCacheLatestWorldRugbyRankings()

return if (success) Result.success() else Result.retry()

}

}

12 of 31

Define some constraints 📱

  • Device must be charging
  • Device must be idle
  • Network connectivity type
  • Battery must not be low
  • Storage must not be low
  • A few others that match JobScheduler

13 of 31

Define some constraints 📱

val constraints = Constraints.Builder()

.setRequiredNetworkType(NetworkType.CONNECTED)

.build()

14 of 31

Create a WorkRequest (one-time) 1️⃣

  • Reference the Worker class
  • Set constraints
  • Optional: set input Data (Map<String, ?>)
  • Optional: add tag/s
  • Optional: set a backoff criteria
    • EXPONENTIAL, LINEAR
    • Duration

15 of 31

Create a WorkRequest (periodic) ⏲️

  • Same as one-time request
  • Adds: repeat interval (minimum 15 minutes)

16 of 31

Create a WorkRequest (periodic) ⏲️

val rankingsWorkRequest =

PeriodicWorkRequestBuilder<RankingsWorker>(1L, TimeUnit.Days)

.setConstraints(constraints)

.build()

// Use OneTimeWorkRequestBuilder for one-time requests

17 of 31

Enqueue your work! 🎉

  • Get an instance of WorkManager
  • Use the WorkRequest
  • Set a unique work name (an identifier of sorts)
  • Choose a policy for existing work
    • REPLACE, KEEP

18 of 31

Enqueue your work! 🎉

val workManager = WorkManager.getInstance()

val uniqueWorkName = “rankings_worker”

workManager.enqueueUniquePeriodicWork(

uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, rankingsWorkRequest

)

// Use enqueueUniqueWork for one-time work

19 of 31

Monitor Worker state 👂

  • Query WorkManager for enqueued work
  • By unique work name or tag
  • Can return multiple results because of common tags or chained work
  • Returns a LiveData<WorkInfo>
  • Check WorkInfo.State on change

20 of 31

Monitor Worker state 👂

val workManager = WorkManager.getInstance()

val uniqueWorkName = “rankings_worker”

val rankingsWorkInfoLiveData =

workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)

// Use getWorkInfosByTagLiveData for tags

rankingsWorkInfoLiveData.observe(lifecycleOwner, Observer { workInfos ->

val workInfo = workInfos?.firstOrNull() ?: return@Observer

when (workInfo.state) {

State.RUNNING -> // Show Snackbar

else -> // Hide Snackbar

})

21 of 31

Monitor Worker state 👂

val workManager = WorkManager.getInstance()

val uniqueWorkName = “rankings_worker”

val rankingsWorkInfoLiveData =

workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)

// Use getWorkInfosByTagLiveData for tags

rankingsWorkInfoLiveData.observe(lifecycleOwner, Observer { workInfos ->

val workInfo = workInfos?.firstOrNull() ?: return@Observer

when (workInfo.state) {

State.RUNNING -> // Show Snackbar

else -> // Hide Snackbar

})

22 of 31

Retrieve output data 🎁

  • Uses the Data class (wrapper for Map<String, ?>)
  • Remember from earlier - Result.success(outputData) returned by Worker.doWork()
  • Use the same approach for worker state and LiveData<WorkInfo>
  • Instead of workInfo.state - workInfo.outputData
  • How long are results kept around for? Use WorkRequest.Builder.keepResultsForAtLeast

23 of 31

Cancel running work ❌

  • Cancel work via instance of WorkManager
  • By unique work name: cancelUniqueWork
  • By tag: cancelAllWorkByTag
  • CAUTION: cancelAllWork

24 of 31

Gotcha: Retrofit network calls 📡

  • There is no asynchronous callback support inside a Worker
  • doWork() needs to return a Result
  • We can’t use Retrofit’s Callback mechanism
  • So then…
  • Instead of: service.call().enqueue(callback)
  • Use: val response = service.call().execute()
  • Handle null/errors/exceptions manually

25 of 31

Dependency injection with Dagger 💉

  • We want to be able to inject certain classes eg. Repositories, UseCases
  • WorkerFactory available for custom class instantiation, some Manifest work too
  • Similar to the ViewModelFactory approach? Nope…
  • Worker constructor includes WorkerParameters which Dagger doesn’t know about

26 of 31

Dependency injection with Dagger 💉

Let’s open up the article...

27 of 31

Incorporating Kotlin Coroutines ↪️

  • Instead of Worker - CoroutineWorker!
  • doWork() is now a suspend fun
  • No need to use runBlocking
  • Internally handles Coroutine context + scopeservice.call().enqueue(callback)

28 of 31

Incorporating Kotlin Coroutines ↪️

// Before

class RankingsWorker(

context: Context,

workerParams: WorkerParameters

private val rankingsRepository: RankingsRepository

) : Worker(context, workerParams) {

override fun doWork() = runBlocking {

fetchAndCacheLatestWorldRugbyRankings()

}

private suspend fun fetchAndCacheLatestWorldRugbyRankings(): Result {

val success = rankingsRepository

.fetchAndCacheLatestWorldRugbyRankings() // Now suspending

return if (success) Result.success() else Result.retry()

}

}

29 of 31

Incorporating Kotlin Coroutines ↪️

// After

class RankingsWorker(

context: Context,

workerParams: WorkerParameters

private val rankingsRepository: RankingsRepository

) : CoroutineWorker(context, workerParams) {

override suspend fun doWork() = fetchAndCacheLatestWorldRugbyRankings()

private suspend fun fetchAndCacheLatestWorldRugbyRankings(): Result {

val success = rankingsRepository

.fetchAndCacheLatestWorldRugbyRankings() // Now suspending

return if (success) Result.success() else Result.retry()

}

}

30 of 31

More resources 📚

31 of 31

Any questions? 🤷‍♂️