class: center, middle, inverse # WorkManager ### Andrey Beryukhov --- # About Me -- - Senior Android Developer -- - Kaspersky Internet Security for Android -- - Public activities - [Modularization article](https://proandroiddev.com/modularization-of-android-applications-in-2021-a79a590d5e5b) - [Compose article](https://proandroiddev.com/change-my-mind-or-android-development-transformation-to-jetpack-compose-coroutines-e719a342cc52) - [Public speeches](https://beryukhov.ru/) -- - Open-source - [Coffeegram](https://github.com/phansier/Coffeegram) (Compose Android & Desktop) - 92 ★ - [FlowReactiveNetwork](https://github.com/phansier/FlowReactiveNetwork) - 62 ★ -- - Android Academy 2020 - mentor --- # Agenda 1. Common concepts -- 2. Three real life samples -- 3. Bonus points to use it --- class: center, middle, inverse_mid # Common concepts ### (For those who did not know or have forgotten) --- # Purpose -- ### Deferrable & Asynchronous tasks -- - not required to run immediately -- - required to run reliably --- background-image: url(images/task-category-tree.png) --- background-image: url(images/task-category-tree-1.png) --- background-image: url(images/task-category-tree-2.png) --- background-image: url(images/task-category-tree-3.png) --- # Advantages -- ### API 14+ -- ### Consistent API --- ## Recommended replacement for all previous Android background scheduling APIs -- - FirebaseJobDispatcher, -- - GcmNetworkManager -- - Still used inside on API 14-22 with Google Services (**work-gcm** artifact) -- - Job Scheduler -- - Still used inside on API 23+ -- - Custom alarm manager & broadcast receiver -- - Used inside on API 14-22 without Google Services --- # Work Constraints -- ### Run when -- - the network is unmetered (e.g. Wi-Fi) -- - the device in Idle -- - it has sufficient storage space or battery level -- - it is charging --- # Robust Scheduling -- - One-time -- - Repeatedly -- - Scheduled work kept in SQLite -- #### WM ensures.red[*] .footnote[.red[*] Here and further: WM - WorkManager, W - Work] -- - W persists -- - W rescheduled after reboot -- - Doze mode --- # Robust Scheduling .pull-left[ - One-time - Repeatedly - Scheduled work kept in SQLite #### WM ensures.red[*] .footnote[.red[*] Here and further: WM - WorkManager, W - Work] - W persists - W rescheduled after reboot - Doze mode ] .pull-right[  ] --- ### Flexible retry policies -- - Exponential backoff policies -- ### Work chaining -- .pull-left[ - Sequentially - In parallel ] -- .pull-right[ ```remark WorkManager.getInstance(...) .beginWith(listOf(workA,workB)) .then(workC) .enqueue() ``` ] -- ### Threading Interoperability -- - RxJava - Coroutines --- background-image: url(images/mm.png) ## More info [Mindmap with documentation in Markdown](https://github.com/phansier/WorkManagerSample/blob/master/Mindmap.md) [Samples code](https://github.com/phansier/WorkManagerSample) .footnote[ [Docs](https://developer.android.com/topic/libraries/architecture/workmanager) [Workmanager - MAD Skills](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_J88-h0PhCO_aV0HIAs9Qk), YouTube video series Google Codelabs [1](https://developer.android.com/codelabs/android-workmanager) [2](https://developer.android.com/codelabs/android-adv-workmanager#) [Another Codelab](https://www.raywenderlich.com/20689637-scheduling-tasks-with-android-workmanager) ] --- class: center, middle, inverse_mid # Real life samples --- # #1 Firebase Remote Config Fetch ```kotlin class RemoteConfigFetcherWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() Result.success() } catch (someExceptionsToRetry) { Result.retry() } catch (someExceptionsToFail) { Result.failure() } } } ``` .footnote[ Inspired by [Firebase Remote Config: 3 lessons learned](https://medium.com/dipien/firebase-remote-config-some-lessons-learned-119a80b66b28) ] --- ```kotlin `class RemoteConfigFetcherWorker`(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() Result.success() } catch (someExceptionsToRetry) { Result.retry() } catch (someExceptionsToFail) { Result.failure() } } } ``` --- ```kotlin class RemoteConfigFetcherWorker(appContext: Context, workerParams: WorkerParameters) : `Worker`(appContext, workerParams) { override fun doWork(): Result { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() Result.success() } catch (someExceptionsToRetry) { Result.retry() } catch (someExceptionsToFail) { Result.failure() } } } ``` --- # 4 work primitives -- ### Worker -- ### CoroutineWorker -- ### RxWorker -- ### ListenableWorker --- # Worker -- - expects to work be done in blocking fashion -- - by default runs W on background thread -- - it comes from the Executor specified in WM's Configuration (can be customized) -- - needs to override onStopped() or call isStopped() to handle stoppages --- # CoroutineWorker -- - by default runs W on dispatcher Default -- .pull-left[ - dispatcher can be customized ] -- .pull-right[ ```kotlin withContext(Dispatchers.IO) {} in doWork() ``` ] -- - needs **work-runtime-ktx** artifact -- - handles stoppages automatically -- - cancels the coroutine --- background-image: url(images/coroutines_meme.png) --- # RxWorker -- .pull-left[ ```kotlin override fun createWork(): Single
``` ] -- - called on main thread -- - subscribed on a background thread by default -- - override RxWorker.getBackgroundScheduler() to change the subscribing thread -- - **work-rxjava2** or **work-rxjava3** artifacts -- - handles stoppages automatically -- - disposes the subscription --- # ListenableWorker -- - Base class for 3 others -- - For use with Java callback-based asynchronous API -- - returns ListenableFuture< Result> -- - needs to override onStopped() or call isStopped() to handle stoppages --- ```kotlin class RemoteConfigFetcherWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override `fun doWork(): Result` { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() Result.success() } catch (someExceptionsToRetry) { Result.retry() } catch (someExceptionsToFail) { Result.failure() } } } ``` --- ```kotlin class RemoteConfigFetcherWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() `Result.success()` } catch (someExceptionsToRetry) { Result.retry() } catch (someExceptionsToFail) { Result.failure() } } } ``` --- ```kotlin class RemoteConfigFetcherWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() Result.success() } catch (someExceptionsToRetry) { Result.retry() } catch (someExceptionsToFail) { `Result.failure()` } } } ``` --- ```kotlin class RemoteConfigFetcherWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { return try { // Block on the task for a maximum of 60 seconds, otherwise time out. val taskResult = Tasks.await(Firebase.remoteConfig.fetchAndActivate(), 60, TimeUnit.SECONDS) saveSuccessFetchInSharedPrefs() Result.success() } catch (someExceptionsToRetry) { `Result.retry()` } catch (someExceptionsToFail) { Result.failure() } } } ``` --- # Work enqueuing (requests) ```kotlin companion object { fun enqueue(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val workRequestBuilder = OneTimeWorkRequestBuilder
() .setConstraints(constraints) WorkManager.getInstance(context).enqueueUniqueWork( uniqueWorkName = RemoteConfigFetcherWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, workRequestBuilder.build() ) } } ``` --- ```kotlin companion object { fun enqueue(context: Context) { val `constraints` = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val workRequestBuilder = OneTimeWorkRequestBuilder
() .setConstraints(constraints) WorkManager.getInstance(context).enqueueUniqueWork( uniqueWorkName = RemoteConfigFetcherWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, workRequestBuilder.build() ) } } ``` --- ```kotlin companion object { fun enqueue(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val workRequestBuilder = `OneTimeWorkRequestBuilder`
() .setConstraints(constraints) WorkManager.getInstance(context).enqueueUniqueWork( uniqueWorkName = RemoteConfigFetcherWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, workRequestBuilder.build() ) } } ``` --- # abstract WorkRequest class -- .pull-left[ - OneTimeWorkRequest ] -- .pull-right[ ```kotlin OneTimeWorkRequest.from(MyWork::class.java) ``` ] -- - PeriodicWorkRequest -- .pull-left[ ```kotlin SomeWorkRequestBuilder
() // Additional configuration .build() ``` ] --- ```kotlin companion object { fun enqueue(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val workRequestBuilder = OneTimeWorkRequestBuilder
() .`setConstraints(constraints)` WorkManager.getInstance(context).enqueueUniqueWork( uniqueWorkName = RemoteConfigFetcherWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, workRequestBuilder.build() ) } } ``` --- ```kotlin companion object { fun enqueue(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val workRequestBuilder = OneTimeWorkRequestBuilder
() .setConstraints(constraints) `WorkManager.getInstance(context).enqueueUniqueWork`( uniqueWorkName = RemoteConfigFetcherWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, workRequestBuilder.build() ) } } ``` --- # Unique Work -- - .enqueue(myWork) -- - .enqueueUniqueWork() - .enqueueUniquePeriodicWork() -- - 3 params -- 1. work -- 2. uniqueWorkName -- - Applied to instance of Work -- 3. existingWorkPolicy --- # WorkPolicies 2 Applicable for both OneTime & Periodic work: 1. REPLACE existing work with the new work 2. KEEP existing work and ignore the new work 2 Applicable only for OneTime 1. APPEND the new work to the end of the existing work - creates a chain - if work1 canceled or failed, work2 too 2. APPEND_OR_REPLACE - Like APPEND, creates a chain - If work1 cancels or failed, work2 still runs (replaces first) --- # #2 Antivirus scan after bases update -- - Periodical work -- - Chain of works -- - Complex constraints -- - Cheap network availability - Device in idle - Device is charging -- - Rx --- # Create Workers -- ```kotlin class UpdateWorker(appContext: Context, workerParams: WorkerParameters) : `RxWorker`(appContext, workerParams) { override fun createWork(): Single
{ return doFakeUpdate().subscribeOn(Schedulers.io()) } } ``` -- ```kotlin class ScanWorker(appContext: Context, workerParams: WorkerParameters) : RxWorker(appContext, workerParams) { override fun createWork(): `Single
` { return doFakeScan().subscribeOn(Schedulers.computation()) } } ``` --- # Create Constraints -- ```kotlin val updateConstraints = Constraints.Builder() `.setRequiredNetworkType(NetworkType.UNMETERED)` .setRequiresStorageNotLow(true) .build() ``` --- # Update Constraints ```kotlin val updateConstraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) `.setRequiresStorageNotLow(true)` .build() ``` --- # Scan Constraints ```kotlin val scanConstraints = Constraints.Builder().apply { if (isOnlyWhileCharging) { `setRequiresCharging(isOnlyWhileCharging)` } else { setRequiresBatteryNotLow(true) } }.build() ``` --- # Scan Constraints ```kotlin val scanConstraints = Constraints.Builder().apply { if (isOnlyWhileCharging) { setRequiresCharging(isOnlyWhileCharging) } else { `setRequiresBatteryNotLow(true)` } }.build() ``` --- # Work constraints .pull-left[ - NetworkType - NOT_REQUIRED - default - CONNECTED - UNMETERED - NOT_ROAMING - METERED - BatteryNotLow.gray[: Boolean] - RequiresCharging.gray[: Boolean] - StorageNotLow.gray[: Boolean] - DeviceIdle.red[*].gray[: Boolean] ] .pull-right[ ```kotlin val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresCharging(true) .build() ``` - All of Constraints should pass (and not or) - Constraint may stop the work in the middle .footnote[.gray[.red[*] @RequiresApi(23) Cannot set backoff criteria & run in foreground with an idle mode ]] ] --- # RequestBuilders ```kotlin val updateWorkRequest = OneTimeWorkRequestBuilder
() .setConstraints(updateConstraints) .setBackoffCriteria( BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES ) .build() val scanWorkRequest = OneTimeWorkRequestBuilder
() .setConstraints(scanConstraints) .setBackoffCriteria( BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES ) .build() ``` --- # Set Constraints ```kotlin val updateWorkRequest = OneTimeWorkRequestBuilder
() `.setConstraints(updateConstraints)` .setBackoffCriteria( BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES ) .build() val scanWorkRequest = OneTimeWorkRequestBuilder
() `.setConstraints(scanConstraints)` .setBackoffCriteria( BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES ) .build() ``` --- # Set Backoff Criteria ```kotlin val updateWorkRequest = OneTimeWorkRequestBuilder
() .setConstraints(updateConstraints) `.setBackoffCriteria( ` ` BackoffPolicy.LINEAR,` ` 10, ` ` TimeUnit.MINUTES ` `) ` .build() val scanWorkRequest = OneTimeWorkRequestBuilder
() .setConstraints(scanConstraints) `.setBackoffCriteria( ` ` BackoffPolicy.LINEAR,` ` 10, ` ` TimeUnit.MINUTES ` `) ` .build() ``` --- # Retry and Backoff policies .pull-left[ - return Result.retry() from worker - Backoff delay - minimum amount of time to wait - default = 10 seconds - Backoff policy - how the backoff delay should increase - 2 types ] .pull-right[ ```kotlin Builder.setBackoffCriteria( BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS) ``` - LINEAR - 10->20->30->40 - EXPONENTIAL - default - 10->20->40->80 ] - In fact delays are always >= to given --- # build() generates Id ```kotlin val updateWorkRequest = OneTimeWorkRequestBuilder
() .setConstraints(updateConstraints) .setBackoffCriteria( BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES ) `.build()` val scanWorkRequest = OneTimeWorkRequestBuilder
() .setConstraints(scanConstraints) .setBackoffCriteria( BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES ) `.build()` ``` --- # Enqueue in chain --- # Enqueue in chain .pull-left[ ```kotlin WorkManager.getInstance(context) ``` ] --- # Enqueue in chain .pull-left[ ```kotlin WorkManager.getInstance(context) .beginUniqueWork( ``` ] --- # Enqueue in chain .pull-left[ ```kotlin WorkManager.getInstance(context) .beginUniqueWork( UpdateWorker::class.java.simpleName, ExistingWorkPolicy.REPLACE, `updateWorkRequest` ) ``` ] --- # Enqueue in chain .pull-left[ ```kotlin WorkManager.getInstance(context) .beginUniqueWork( UpdateWorker::class.java.simpleName, ExistingWorkPolicy.REPLACE, updateWorkRequest ) .then(`scanWorkRequest`) ``` ] --- # Enqueue in chain .pull-left[ ```kotlin WorkManager.getInstance(context) .beginUniqueWork( UpdateWorker::class.java.simpleName, ExistingWorkPolicy.REPLACE, updateWorkRequest ) .then(scanWorkRequest) .enqueue() ``` ] --- # Enqueue in chain .pull-left[ ```kotlin WorkManager.getInstance(context) .beginUniqueWork( UpdateWorker::class.java.simpleName, ExistingWorkPolicy.REPLACE, updateWorkRequest ) .then(scanWorkRequest) .enqueue() ``` ] .pull-right[  ] --- # Chains .pull-left[ 1. WM..red[beginWith](OTWR .red[*] or list< OTWR>): WorkContinuation 2. WorkContinuation..red[then](OTWR or list<>): WorkContinuation 3. WorkContinuation..red[enqueue()] ] .pull-right[ ```kotlin WorkManager.getInstance(myContext) // Candidates to run in parallel .beginWith(listOf(plantName1, plantName2)) // Dependent work (only runs after all previous) .then(cache) .then(upload) // Call enqueue to kick things off .enqueue() ``` ]
- List means parallel works .footnote[.red[*] OTWR - OneTimeWorkRequest] --- # Another chain .left[ ```kotlin WorkManager.getInstance(context) .beginWith(`update`WorkRequest.build()) .then(listOf(`scan`WorkRequest.build(), `frc`workRequest.build())) .then(listOf(`frc`workRequest.build(), `scan`WorkRequest.build())) .then(`update`WorkRequest.build()) .enqueue() ``` ] --- # Another chain .left[ ```kotlin WorkManager.getInstance(context) .beginWith(`update`WorkRequest.build()) .then(listOf(`scan`WorkRequest.build(), `frc`workRequest.build())) .then(listOf(`frc`workRequest.build(), `scan`WorkRequest.build())) .then(`update`WorkRequest.build()) .enqueue() ``` ] .right[  ] --- # Input & Output data -- - key-value pairs in a Data object -- - Builder.setInputData() -- - Worker.getInputData() -- - from Worker return Result.success\\failure(**outputData**) -- - workInfo.getOutputData() --- # .pull-right[  ] --- # InputMerger .pull-left[ - for handling results of previous parallel Works - OverwritingInputMerger - merges keys and overwrites (by last completed) if repeated - no guarantees of order - ArrayCreatingInputMerger - merges keys and creates array (of values for a key) if repeated - Custom implementation possible ] .pull-right[  ] --- # Progress tracking
--- # Progress tracking #1 .pull-left[ ```kotlin OneTimeWorkRequestBuilder
() .setConstraints(scanConstraints) .setBackoffCriteria(...) `.addTag(TAG_SCAN_PROGRESS)` .build() ``` ] .pull-right[
] --- # Progress tracking #2 .pull-left[ ```kotlin override fun createWork(): Single
{ return Observable.interval(1, TimeUnit.SECONDS) .take(10) .map { it * 10 } .doOnEach { setProgressAsync( workDataOf(PROGRESS to it.value) ) } .collectInto(Result.success()) { res, _ -> res } .subscribeOn(Schedulers.computation()) } ``` ] .pull-right[
] --- # Progress tracking #2 .pull-left[ ```kotlin override fun createWork(): Single
{ return `Observable.interval(1, TimeUnit.SECONDS)` `.take(10) ` `.map { it * 10 } ` .doOnEach { setProgressAsync( workDataOf(PROGRESS to it.value) ) } .collectInto(Result.success()) { res, _ -> res } .subscribeOn(Schedulers.computation()) } ``` ] .pull-right[
] --- # Progress tracking #2 .pull-left[ ```kotlin override fun createWork(): Single
{ return Observable.interval(1, TimeUnit.SECONDS) .take(10) .map { it * 10 } .doOnEach { `setProgressAsync(` ` workDataOf(PROGRESS to it.value)` `)` } .collectInto(Result.success()) { res, _ -> res } .subscribeOn(Schedulers.computation()) } ``` ] .pull-right[
] --- # Progress tracking #3 .pull-left[ ```kotlin val ld: LiveData
> = WorkManager.getInstance(context) `.getWorkInfosByTagLiveData(TAG_SCAN_PROGRESS)` ``` ] .pull-right[
] --- background-image: url(images/livedata_meme.png) --- # Progress tracking #3 .pull-left[ ```kotlin val ld: LiveData
> = WorkManager.getInstance(context) `.getWorkInfosByTagLiveData(TAG_SCAN_PROGRESS)` ``` ] .pull-right[
] .pull-left[ ```kotlin val list: List
? `by ld.observeAsState()` ``` ] -- .pull-left[ ```kotlin val value: WorkInfo? = list?.firstOrNull { it.state == WorkInfo.`State.RUNNING` } ``` ] -- .pull-left[ ```kotlin if (value != null) { ScanProgress( `value.progress.`getLong(PROGRESS, defaultValue = 0) / 100f ) } ``` ] --- # Progress tracking #3 .pull-left[ ```kotlin val ld: LiveData
> = WorkManager.getInstance(context) `.getWorkInfosByTagLiveData(TAG_SCAN_PROGRESS)` ``` ] .pull-right[
] .pull-left[ ```kotlin val list: List
? `by ld.observeAsState()` ``` ] .pull-left[ ```kotlin val value: WorkInfo? = list?.firstOrNull { it.state == WorkInfo.`State.RUNNING` } ``` ] .pull-left[ ```kotlin if (value != null) { ScanProgress( value.progress.`getLong(PROGRESS`, defaultValue = 0) / 100f ) } ``` ] --- class: center, middle, inverse
--- # #3 Making scan periodical -- .pull-left[ ```kotlin OneTimeWorkRequestBuilder
``` ] -- - Chains only for OneTimeWork -- - PeriodicWork can launch more complex OneTimeWork (from background) --- # PeriodicScanWorker -- ```kotlin override suspend fun doWork(): Result { `enqueueUpdateScan`(appContext, isOnlyWhileCharging) return Result.success() } ``` -- ```kotlin val periodicWorkRequest = PeriodicWorkRequestBuilder
( repeatInterval = 1, // MIN_PERIODIC_INTERVAL_MILLIS = 15 minutes. repeatIntervalTimeUnit = TimeUnit.DAYS, flexTimeInterval = 1, // MIN_PERIODIC_FLEX_MILLIS = 5 minutes. flexTimeIntervalUnit = TimeUnit.HOURS ) .setInitialDelay(10, TimeUnit.MINUTES) .addTag(`PERIODIC_SCAN_WORK_TAG`) .build() ``` -- ```kotlin fun cancel(context: Context) { WorkManager.getInstance(context).`cancelAllWorkByTag(PERIODIC_SCAN_WORK_TAG)` } ``` --- # Intervals -- .pull-left[ - interval = minimum time between repetitions
] .pull-right[ ```kotlin repeatInterval = 1, // >= 15 minutes repeatIntervalTimeUnit = TimeUnit.DAYS ``` ] -- .pull-left[ - flex period = repeatInterval - flexInterval
] .pull-right[ ```kotlin flexTimeInterval = 1, // >= 5 minutes. flexTimeIntervalUnit = TimeUnit.HOURS ``` ] --  -- - Work Constraints > Periodic => Work may be skipped -- .pull-left[ - Delay - Will be applied only to first launch ] .pull-right[ ```kotlin .setInitialDelay(10, TimeUnit.MINUTES) ``` ] --- class: center, middle, inverse_mid # Some more points to use it --- # Testing .pull-left[ - Unit testing - TestWorkerBuilder - TestListenableWorkerBuilder ] -- .pull-right[ ```kotlin @Test fun testSleepWorker() { val worker = `TestWorkerBuilder`
( context = context, executor = executor ).build() val result = worker.`doWork`() assertThat(result, is(Result.success())) } ``` ] -- .pull-left[ - Integration testing - [More info](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing) ] --- # Debugging -- - API 26+ - Android Studio Canary --  --- # Debugging #1 -- ADB -- ```shell adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" -p "
" ``` -- - Work requests that were completed in the last 24 hours. -- - Work requests that are currently running. -- - Scheduled work requests --- # Debugging #2 -- Debug logs with log-tag prefix WM- -- - Default WorkManagerInitializer should be removed from AndroidManifest -- ```xml
``` --- # Debugging #2 Debug logs with log-tag prefix WM- - Default WorkManagerInitializer should be removed from AndroidManifest ```xml
``` -- - Custom (on demand) configuration -- ```kotlin class MyApplication() : Application(), `Configuration.Provider` { ``` --- # Debugging #2 Debug logs with log-tag prefix WM- - Default WorkManagerInitializer should be removed from AndroidManifest ```xml
``` - Custom (on demand) configuration ```kotlin class MyApplication() : Application(), Configuration.Provider { override fun getWorkManagerConfiguration() = Configuration.Builder() `.setMinimumLoggingLevel(android.util.Log.DEBUG)` .build() } ``` -- - May also be useful for delayed initialization & improving app start time --- # Debugging #3 -- - API 23+ -- ```shell adb shell dumpsys jobscheduler ``` -- - For every job lists constraints that are - required, - satisfied, - unsatisfied. -- - [More info](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/debugging#request-diagnostic-information-from-workmanager-2.4.0) --- # Long running workers -- - Default work manager execution deadline is 10 minutes -- - After it will be stopped by system -- - What about works that can run longer than 10 minutes? -- - bulk uploads or downloads - crunching on an ML model locally --- # Long running workers -- - WM can manage and run a foreground service -- - showing a configurable notification -- ```kotlin override fun createWork(): Single
{ return Observable.interval(1, TimeUnit.SECONDS) .take(10) .map { it * 10 } .doOnEach { setProgressAsync(workDataOf(PROGRESS to it.value)) `setForegroundAsync`(createForegroundInfo("In progress... ${it.value} %")) } .collectInto(Result.success()) { result, _ -> result } .subscribeOn(Schedulers.computation()) } ``` --- # Long running workers .pull-left[ - WM can manage and run a foreground service - showing a configurable notification ] .pull-right[  ] --- # Long running workers ```kotlin private fun createForegroundInfo(progress: String): ForegroundInfo { ... val intent = WorkManager.getInstance(applicationContext) `.createCancelPendingIntent(getId())` ``` -- ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createChannel() } ``` -- ```kotlin val notification = NotificationCompat.Builder(applicationContext, id) .set(*) .addAction(android.R.drawable.ic_delete, cancel, `intent`) .build() return ForegroundInfo(notification) } ``` --- # Foreground services limitations -- - target 29+ -- - location access -- - target 30+ -- - camera or microphone access -- - Need to declare foreground service type -- ```xml
``` --- # Foreground services limitations - target 29+ - location access - target 30+ - camera or microphone access - Need to declare foreground service type ```xml
``` -- - specify foreground service type in setForeground() --- background-image: url(images/mm.png) ## More info [Mindmap with documentation in Markdown](https://github.com/phansier/WorkManagerSample/blob/master/Mindmap.md) [Samples code](https://github.com/phansier/WorkManagerSample) .footnote[ [Docs](https://developer.android.com/topic/libraries/architecture/workmanager) [Workmanager - MAD Skills](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_J88-h0PhCO_aV0HIAs9Qk), YouTube video series Google Codelabs [1](https://developer.android.com/codelabs/android-workmanager) [2](https://developer.android.com/codelabs/android-adv-workmanager#) [Another Codelab](https://www.raywenderlich.com/20689637-scheduling-tasks-with-android-workmanager) ] --- class: center, middle, inverse # Questions? .footnote_center[ ### Andrey Beryukhov [@phansier](https://t.me/phansier) [Slides](https://beryukhov.ru/workmanager) ]