Slide 1

Slide 1 text

Workout your tasks with WorkManager Magda Miu @magdamiu Squad Lead Developer at Orange Android Google Developer Expert

Slide 2

Slide 2 text

A User’s Nightmare...

Slide 3

Slide 3 text

A User’s Nightmare...

Slide 4

Slide 4 text

A User’s Nightmare...

Slide 5

Slide 5 text

53% Users who uninstalled a mobile app with crashes, freezes or errors

Slide 6

Slide 6 text

Users who stopped to use a mobile app due to heavy battery usage 36%

Slide 7

Slide 7 text

1 4 3 2 Agenda Android Memory Model Background Processing Solutions WorkManager Summary

Slide 8

Slide 8 text

1 4 3 2 Agenda Android Memory Model Background Processing Solutions WorkManager Summary

Slide 9

Slide 9 text

Android is for everyone

Slide 10

Slide 10 text

Android is for everyone apps

Slide 11

Slide 11 text

Android is for everyone apps devices

Slide 12

Slide 12 text

Android is for everyone apps devices users

Slide 13

Slide 13 text

Android is for everyone apps devices users

Slide 14

Slide 14 text

Android is for everyone apps devices users

Slide 15

Slide 15 text

Physical memory on device Used pages Memory actively being used Cached pages Memory backed by device storage. Can be reclaimed if needed. Free pages Memory not currently being used for anything. Source: Understanding Android memory usage (Google I/O '18)

Slide 16

Slide 16 text

memory free

Slide 17

Slide 17 text

memory free page reclaim (kswapd)

Slide 18

Slide 18 text

memory free page reclaim (kswapd) low memory killer (lmk)

Slide 19

Slide 19 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached Source: Understanding Android memory usage (Google I/O '18)

Slide 20

Slide 20 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached

Slide 21

Slide 21 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached

Slide 22

Slide 22 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached

Slide 23

Slide 23 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached

Slide 24

Slide 24 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached

Slide 25

Slide 25 text

Android process list Native Init kswapd netd logd adb ... System system_server Persistent Foreground Perceptible Service Home Previous Cached

Slide 26

Slide 26 text

Android battery optimizations

Slide 27

Slide 27 text

Android battery optimizations Doze mode App standby

Slide 28

Slide 28 text

Android battery optimizations Doze mode App standby Doze on the go

Slide 29

Slide 29 text

Android battery optimizations Doze mode App standby Doze on the go Background service limitations

Slide 30

Slide 30 text

Android battery optimizations Doze mode App standby Doze on the go Background service limitations App standby buckets

Slide 31

Slide 31 text

1 4 3 2 Agenda Android Memory Model Background Processing Solutions WorkManager Summary

Slide 32

Slide 32 text

Exact Timing Deferrable Guaranteed Execution Best-Effort Credit: Android Jetpack: easy background processing with WorkManager (Google I/O '18) time/importance

Slide 33

Slide 33 text

Exact Timing Deferrable Guaranteed Execution Best-Effort ThreadPool Coroutines RxJava ThreadPool Coroutines RxJava JobScheduler JobDispatcher AlarmManager BroadcastReceivers Foreground Service time/importance

Slide 34

Slide 34 text

Exact Timing Deferrable Guaranteed Execution Best-Effort ThreadPool Coroutines RxJava ThreadPool Coroutines RxJava JobScheduler JobDispatcher AlarmManager BroadcastReceivers WorkManager Foreground Service time/importance

Slide 35

Slide 35 text

4 3 Agenda WorkManager Summary 1 2 Android Memory Model Background Processing Solutions

Slide 36

Slide 36 text

“WorkManager is a library for managing deferrable and guaranteed background work.”

Slide 37

Slide 37 text

Why to use WorkManager? ● Backward compatibility with different OS versions (API level 14+) ● Follows system health best practices ● Supports one-off and periodic tasks ● Supports chained tasks with input/output ● Defines constraints on when the task runs ● Guarantees task execution, even if the app or device restarts

Slide 38

Slide 38 text

Fetching data periodically Uploading images/files to a server Syncing data between app and server Sending logs to a server Executing expensive operations on data When to use WorkManager?

Slide 39

Slide 39 text

Dependencies dependencies { def work_version = "2.2.0" implementation "androidx.work:work-runtime-ktx:$work_version" }

Slide 40

Slide 40 text

WorkManager - main components Worker WorkRequest WorkManager

Slide 41

Slide 41 text

Create Worker JobScheduler API 23+? AlarmManager + Broadcast Receiver NO YES Play Services? Firebase JobDispatcher NO YES Internal mechanism

Slide 42

Slide 42 text

Worker

Slide 43

Slide 43 text

Worker doWork() Result FAILURE SUCCESS RETRY

Slide 44

Slide 44 text

class SyncWorker(c: Context, wp: WorkerParameters):Worker(c, wp) { override fun doWork(): Result { loadData() return Result.success() } }

Slide 45

Slide 45 text

class SyncWorker(c: Context, wp: WorkerParameters):Worker(c, wp) { override fun doWork(): Result { loadData() return Result.success() } } Runs synchronously on a background thread

Slide 46

Slide 46 text

WorkRequest

Slide 47

Slide 47 text

Create Worker OneTimeWorkRequest Is it periodic ? PeriodicWorkRequest NO YES

Slide 48

Slide 48 text

val syncOnlyOnce = OneTimeWorkRequestBuilder().build() val syncPeriodically = PeriodicWorkRequestBuilder(1, TimeUnit.HOURS).build()

Slide 49

Slide 49 text

OneTimeWorkRequest ● It is used for non-repeating work ● It could have an initial delay ● It could be part of a chain or graph of work

Slide 50

Slide 50 text

PeriodicWorkRequest ● Used for tasks that need to execute periodically ● It could have an initial delay ● The minimum repeat interval that can be defined is 15 minutes (same as the JobScheduler API) ● It cannot be part of a chain or graph of work ● The execution may be delayed because WorkManager is subject to OS battery optimizations, such as doze mode

Slide 51

Slide 51 text

val periodicRefreshRequest = PeriodicWorkRequest.Builder( SyncWorker::class.java, // the worker class 30, // repeating interval TimeUnit.Minutes, 15, // flex interval - worker will run somehow within this period of time, but at the end of repeating interval TimeUnit.MINUTES )

Slide 52

Slide 52 text

Constraints

Slide 53

Slide 53 text

Set constraints setRequiredNetworkType(requiredNetworkType: NetworkType) setRequiresBatteryNotLow(requiresBatteryNotLow: Boolean) setRequiresCharging(requiresCharging: Boolean) setRequiresDeviceIdle(requiresDeviceIdle: Boolean) setRequiresStorageNotLow(requiresStorageNotLow: Boolean)

Slide 54

Slide 54 text

NetworkType CONNECTED // Any working network connection METERED // A metered network connection NOT_REQUIRED // A network is not required for this work. NOT_ROAMING // A non-roaming network connection UNMETERED // An unmetered network connection

Slide 55

Slide 55 text

val constraints = Constraints.Builder() .setRequiresCharging(true) .setRequiresStorageNotLow(true) .setRequiredNetworkType(NetworkType.CONNECTED) .build() val syncOnlyOnce = OneTimeWorkRequestBuilder() .setConstraints(constraints) .build()

Slide 56

Slide 56 text

Input & Output Data

Slide 57

Slide 57 text

putBoolean / getBoolean putBooleanArray / getBooleanArray putDouble / getDouble putDoubleArray / getDoubleArray putFloat / getFloat putFloatArray / getFloatArray putInt / getInt putIntArray / getIntArray putLong / getLong putLongArray / getLongArray putString / getString putStringArray / getStringArray putByte / getByte putByteArray / getByteArray Data and Data.Builder* (key-value pairs)

Slide 58

Slide 58 text

val userIdData = Data.Builder() .putInt(DATA_USER_ID, userId) .build() val syncOnlyOnce = OneTimeWorkRequestBuilder() .setInputData(userIdData) .build() val userIdInput = inputData.getInt(Constants.DATA_USER_ID, 0) // ktx val outputData = workDataOf(Constants.DATA_SENT to isDataSent)

Slide 59

Slide 59 text

Tagging Work

Slide 60

Slide 60 text

val userIdData = Data.Builder() .putInt(DATA_USER_ID, userId) .build() val userIdInput = inputData.getInt(Constants.DATA_USER_ID, 0) val syncOnlyOnce = OneTimeWorkRequestBuilder() .setInputData(userIdData) .addTag(Constants.WORKER_SYNC) .build()

Slide 61

Slide 61 text

val userIdData = Data.Builder() .putInt(DATA_USER_ID, userId) .build() val userIdInput = inputData.getInt(Constants.DATA_USER_ID, 0) val syncOnlyOnce = OneTimeWorkRequestBuilder() .setInputData(userIdData) .addTag(Constants.WORKER_SYNC) .addTag(Constants.ONE_SYNC) .build()

Slide 62

Slide 62 text

Unique Work

Slide 63

Slide 63 text

WorkManager.getInstance(context) .enqueueUniquePeriodicWork( Constants.UNIQUE_NAME, ExistingPeriodicWorkPolicy.KEEP, syncPeriodically ) Unique work sequence

Slide 64

Slide 64 text

WorkManager.getInstance(context) .beginUniqueWork(Constants.UNIQUE_NAME, ExistingWorkPolicy.REPLACE, task1) .then(task2) .then(task3) .enqueue() Unique work chain

Slide 65

Slide 65 text

Existing Work Policy enums ● KEEP - keeps the existing unfinished WorkRequest. Enqueues it if one does not already exist. ● REPLACE - always replace the WorkRequest. Cancels and deletes the old one, if it exists. ● APPEND - appends work to an existing chain or create a new chain. KEEP + REPLACE + APPEND = ExistingWorkPolicy KEEP + REPLACE = ExistingPeriodicWorkPolicy

Slide 66

Slide 66 text

Delays and Retries

Slide 67

Slide 67 text

/* Used to indicate that WorkManager should increase the backoff time exponentially */ EXPONENTIAL /* Used to indicate that WorkManager should increase the backoff time linearly */ LINEAR BackoffPolicy enum

Slide 68

Slide 68 text

// add initial delay for OneTimeWorkRequest val syncOnlyOnce = OneTimeWorkRequestBuilder() .setInitialDelay(15, TimeUnit.MINUTES) .build() // backoff delay and policy val syncOnlyOnce = OneTimeWorkRequestBuilder() .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MICROSECONDS) .build()

Slide 69

Slide 69 text

Work Status

Slide 70

Slide 70 text

WorkInfo UUID State Data Tags BLOCKED CANCELLED ENQUEUED FAILED RUNNING SUCCEEDED

Slide 71

Slide 71 text

WorkInfo Id WorkManager.getWorkInfoById(UUID) WorkManager.getWorkInfoByIdLiveData(UUID)

Slide 72

Slide 72 text

WorkInfo Id WorkManager.getWorkInfoById(UUID) WorkManager.getWorkInfoByIdLiveData(UUID) Tag WorkManager.getWorkInfosByTag(String) WorkManager.getWorkInfosByTagLiveData(String)

Slide 73

Slide 73 text

WorkInfo Unique Name WorkManager.getWorkInfosForUniqueWork(String) WorkManager.getWorkInfosForUniqueWorkLiveData(String) Id WorkManager.getWorkInfoById(UUID) WorkManager.getWorkInfoByIdLiveData(UUID) Tag WorkManager.getWorkInfosByTag(String) WorkManager.getWorkInfosByTagLiveData(String)

Slide 74

Slide 74 text

LiveData & WorkManager

Slide 75

Slide 75 text

WorkManager.getInstance().getWorkInfoByIdLiveData(syncOnlyOnce.id) .observe(this, Observer { workInfo -> if (workInfo != null && workInfo.state == State.SUCCEEDED) { displayMessage("Sync finished!") } }) Id

Slide 76

Slide 76 text

WorkManager.getInstance().getWorkInfoByIdLiveData(syncOnlyOnce.id) .observe(this, Observer { workInfo -> if (workInfo != null && workInfo.state == State.SUCCEEDED) { displayMessage("Sync finished!") } }) LiveData Id

Slide 77

Slide 77 text

WorkManager.getInstance() .getWorkInfosByTagLiveData(Constants.TAG_SYNC) .observe(this, Observer> { workStatusList -> val currentWorkStatus = workStatusList ?.getOrNull(0) if (currentWorkStatus ?.state ?.isFinished == true) { displayMessage("Sync finished!") } }) Tag

Slide 78

Slide 78 text

WorkManager.getInstance() .getWorkInfosByTagLiveData(Constants.TAG_SYNC) .observe(this, Observer> { workStatusList -> val currentWorkStatus = workStatusList ?.getOrNull(0) if (currentWorkStatus ?.state ?.isFinished == true) { displayMessage("Sync finished!") } }) LiveData> Tag

Slide 79

Slide 79 text

WorkManager.getInstance() .getWorkInfosForUniqueWorkLiveData(Constants.UNIQUE_NAME) .observe(this, Observer> { workStatusList -> val currentWorkStatus = workStatusList ?.getOrNull(0) if (currentWorkStatus ?.state ?.isFinished == true) { displayMessage("Sync finished!") } }) Unique name

Slide 80

Slide 80 text

WorkManager.getInstance() .getWorkInfosForUniqueWorkLiveData(Constants.UNIQUE_NAME) .observe(this, Observer> { workStatusList -> val currentWorkStatus = workStatusList ?.getOrNull(0) if (currentWorkStatus ?.state ?.isFinished == true) { displayMessage("Sync finished!") } }) LiveData> Unique name

Slide 81

Slide 81 text

Happy path - One Time Work BLOCKED RUNNING SUCCEEDED ENQUEUED

Slide 82

Slide 82 text

“Unhappy” path - One Time Work FAILED CANCELLED CANCEL FAILURE RETRY BLOCKED RUNNING ENQUEUED SUCCEEDED SUCCESS

Slide 83

Slide 83 text

“Unhappy” path - Periodic Work RUNNING ENQUEUED CANCELLED CANCEL SUCCESS RETRY, FAILURE

Slide 84

Slide 84 text

WorkManager

Slide 85

Slide 85 text

enqueue beginWith beginUniqueWork cancel WorkManager

Slide 86

Slide 86 text

Chaining Tasks Task 1 Task 2 Task 3 Input Output Input Output Input Output Outputs are Inputs to child tasks

Slide 87

Slide 87 text

WorkManager.getInstance(context) .beginWith(task1) .then(task2) .then(task3) .enqueue()

Slide 88

Slide 88 text

WorkManager.getInstance(context) .beginWith(task1) .then(task2) .then(task3) .enqueue() WorkContinuation

Slide 89

Slide 89 text

Task 1 Task 2 Task 3

Slide 90

Slide 90 text

WorkManager.getInstance(context) .beginWith(listOf(task1, task2)) .then(task3) .enqueue() WorkContinuation Parallel execution

Slide 91

Slide 91 text

Task 1 Task 2 Task 3 Task 4 Task 5

Slide 92

Slide 92 text

val leftChain = WorkManager.getInstance(context) .beginWith(task1) .then(task2) val rightChain = WorkManager.getInstance(context) .beginWith(task3) .then(task4) val resultChain = WorkContinuation .combine(listOf(leftChain, rightChain)) .then(task5) resultChain.enqueue()

Slide 93

Slide 93 text

InputMerger +merge(): Data ArrayCreatingInputMerger OverwritingInputMerger

Slide 94

Slide 94 text

Key Value user_id 23 name “Android Pie” ArrayCreatingInputMerger

Slide 95

Slide 95 text

Key Value user_id 23 name “Android Pie” Key Value user_id 37 name “Android 10” token “211-war-qw2-akjda” ArrayCreatingInputMerger

Slide 96

Slide 96 text

Key Value user_id 23 name “Android Pie” Key Value user_id 37 name “Android 10” token “211-war-qw2-akjda” ArrayCreatingInputMerger Key Value user_id {23, 37} name {“Android Pie”, “Android 10”} token {“211-war-qw2-akjda”}

Slide 97

Slide 97 text

Key Value user_id 23 name “Android Pie” Key Value user_id “37” name “Android 10” token “211-war-qw2-akjda” ArrayCreatingInputMerger

Slide 98

Slide 98 text

Key Value user_id 23 name “Android Pie” Key Value user_id “37” name “Android 10” token “211-war-qw2-akjda” ArrayCreatingInputMerger Key Value user_id IllegalArgumentException name {“Android Pie”, “Android 10”} token {“211-war-qw2-akjda”}

Slide 99

Slide 99 text

Key Value user_id 23 name “Android Pie” points 350 OverwritingInputMerger

Slide 100

Slide 100 text

Key Value user_id 23 name “Android Pie” points 350 Key Value user_id “37” name “Android 10” token “211-war-qw2-akjda” OverwritingInputMerger

Slide 101

Slide 101 text

Key Value user_id 23 name “Android Pie” points 350 Key Value user_id “37” name “Android 10” token “211-war-qw2-akjda” OverwritingInputMerger Key Value user_id 23 name “Android Pie” points 350

Slide 102

Slide 102 text

Key Value user_id 23 name “Android Pie” points 350 Key Value user_id “37” name “Android 10” token “211-war-qw2-akjda” OverwritingInputMerger Key Value user_id “37” name “Android 10” points 350 token “211-war-qw2-akjda”

Slide 103

Slide 103 text

val merge = OneTimeWorkRequestBuilder() .setInputMerger(ArrayCreatingInputMerger::class.java) .setConstraints(constraints) .build()

Slide 104

Slide 104 text

// running work WorkManager.getInstance(context).enqueue(syncOnlyOnce) // cancelling work WorkManager.getInstance().cancelAllWork() WorkManager.getInstance().cancelAllWorkByTag(Constants.TAG_SYNC) WorkManager.getInstance().cancelUniqueWork(Constants.UNIQUE_NAME) WorkManager.getInstance().cancelWorkById(UUID.fromString(Constants .UNIQUE_UDID))

Slide 105

Slide 105 text

// running work WorkManager.getInstance(context).enqueue(syncOnlyOnce) // cancelling work WorkManager.getInstance(context).cancelAllWork() WorkManager.getInstance().cancelAllWorkByTag(Constants.TAG_SYNC) WorkManager.getInstance().cancelUniqueWork(Constants.UNIQUE_NAME) WorkManager.getInstance().cancelWorkById(UUID.fromString(Constants .UNIQUE_UDID))

Slide 106

Slide 106 text

// running work WorkManager.getInstance(context).enqueue(syncOnlyOnce) // cancelling work WorkManager.getInstance(context).cancelAllWork() WorkManager.getInstance(context).cancelAllWorkByTag(Constants.TAG_SYNC) WorkManager.getInstance().cancelUniqueWork(Constants.UNIQUE_NAME) WorkManager.getInstance().cancelWorkById(UUID.fromString(Constants .UNIQUE_UDID))

Slide 107

Slide 107 text

// running work WorkManager.getInstance(context).enqueue(syncOnlyOnce) // cancelling work WorkManager.getInstance(context).cancelAllWork() WorkManager.getInstance(context).cancelAllWorkByTag(Constants.TAG_SYNC) WorkManager.getInstance(context).cancelUniqueWork(Constants.UNIQUE_NAME) WorkManager.getInstance().cancelWorkById(UUID.fromString(Constants .UNIQUE_UDID))

Slide 108

Slide 108 text

// running work WorkManager.getInstance(context).enqueue(syncOnlyOnce) // cancelling work WorkManager.getInstance(context).cancelAllWork() WorkManager.getInstance(context).cancelAllWorkByTag(Constants.TAG_SYNC) WorkManager.getInstance(context).cancelUniqueWork(Constants.UNIQUE_NAME) WorkManager.getInstance(context).cancelWorkById(UUID.fromString(Constants .UNIQUE_UDID))

Slide 109

Slide 109 text

Threading options 1. ListenableWorker 2. Worker 3. CoroutineWorker 4. RxWorker

Slide 110

Slide 110 text

Threading in WorkManager Worker WorkRequest Executor Internal TaskExecutor Database enqueue execute Constraints met WorkerFactory Credit: Working with WorkManager Presentation Android Developer Summit 2018

Slide 111

Slide 111 text

androidx.work.workdb

Slide 112

Slide 112 text

WorkSpec table

Slide 113

Slide 113 text

ListenableWorker +startWork(): ListenableFuture Worker CoroutineWorker RxWorker

Slide 114

Slide 114 text

ListenableWorker Overview ● A ListenableWorker only signals when the work should start and stop ● The start work signal is invoked on the main thread, so we go to a background thread of our choice manually ● A ListenableFuture is a lightweight interface: it is a Future that provides functionality for attaching listeners and propagating exceptions Stop work ● It is always cancelled when the work is expected to stop. Use a CallbackToFutureAdapter to add a cancellation listener

Slide 115

Slide 115 text

Worker Overview ● Worker.doWork() is called on a background thread, synchronously ● The background thread comes from the Executor specified in WorkManager's Configuration, but it could also be customised Stop work ● Worker.onStopped() is called. This method could be overridden or we could call Worker.isStopped() to checkpoint the code and free up resources when necessary

Slide 116

Slide 116 text

CoroutineWorker Overview ● For Kotlin users, WorkManager provides first-class support for coroutines ● Instead of extending Worker, we should extend CoroutineWorker ● CoroutineWorker.doWork() is a suspending function ● The code runs on Dispatchers.Default, not on Executor (customisation by using CoroutineContext) Stop work ● CoroutineWorkers handle stoppages automatically by cancelling the coroutine and propagating the cancellation signals

Slide 117

Slide 117 text

RxWorker Overview ● For RxJava2 users, WorkManager provides interoperability ● Instead of extending Worker, we should extend RxWorker ● RxWorker.createWork() method returns a Single indicating the Result of the execution, and it is called on the main thread, but the return value is subscribed on a background thread by default. Override RxWorker.getBackgroundScheduler() to change the subscribing thread. Stop work ● Done by default

Slide 118

Slide 118 text

4 Agenda Summary 1 3 2 Android Memory Model Background processing solutions WorkManager

Slide 119

Slide 119 text

WorkManager - Recap ● WorkManager is a wrapper for the existing background processing solutions ● Create one time or periodic work requests ● Identify our tasks by using ids, tags and unique names ● Add constraint, delay and retry policy ● Use input/output data and merge them ● Create chains of tasks ● Use the available threading options

Slide 120

Slide 120 text

Resources ● Official Documentation and Official Reference ● Working with WorkManager (Android Dev Summit ’18) ● Android Jetpack: easy background processing with WorkManager (Google I/O '18) ● https://medium.com/@magdamiu ● Icons from https://www.flaticon.com

Slide 121

Slide 121 text

Thank you! Magda Miu @magdamiu Squad Lead Developer at Orange Android Google Developer Expert