With recent Android releases dealing with background, it has become more complicated than ever. Much like the Star Wars plot, it has become more and more intertwined And so, Google has released WorkManager as part of JetPack — to help us to deal with such a background.
Before learning what WorkManager is — it’s essential to understand why we need it and what was a reasons behind building it. The developers who knows what happens under the hood of the components/libraries and understand why they use it — is by far a better developers.
It’s going to be a long article, so prepare a good cup of coffee & a couple of cookies.
It build in 3 parts:
Part #1 — Memory Basics
Part #2 — Existing Background Solutions
Part #3 — WorkManager
Firstly, before starting with all background-ish things — we need to understand some of the basics of Android processing memory management.
And this is will be our first part:
Part 1: Android Memory 101
A long, long time ago, in a galaxy far far away — Android kernel was first developed it and it was based on Linux-Kernel. The main difference between Android and all other Linux-Kernel based systems is that Android does not have a thing called “Swap space.”
Swap space in Linux is used when the amount of physical memory (RAM) is full. If the system needs more memory resources and the RAM is full, inactive pages in memory are moved to the swap space. While swap space can help machines with a small amount of RAM, it should not be considered a replacement for more RAM. Swap space is located on hard drives, which have a slower access time than physical memory
In Android, there is no such a thing as a “Swap space.” When the system is running out of memory, it’s using OOM Killer to save a galaxy.
The guy’s goal is to free up memory by killing the processes based on their “visibility state” and amount of memory consumed.
Every process gave its
oom_adj score by Activity Manager. It’s a combination of application state (e.g., foreground, background, background with service, etc.). Here is a short example of all
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel. These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0
setprop ro.VISIBLE_APP_ADJ 1
setprop ro.SECONDARY_SERVER_ADJ 2
setprop ro.BACKUP_APP_ADJ 2
setprop ro.HOME_APP_ADJ 4
setprop ro.HIDDEN_APP_MIN_ADJ 7
setprop ro.CONTENT_PROVIDER_ADJ 14
setprop ro.EMPTY_APP_ADJ 15
Higher values of
omm_adj are more likely to be killed by the kernel’s oom killer. The current foreground app has an
omm_adj of 0.
The OOM killer uses configurable rules based on free memory and
omm_adj thresholds. ie, rules state “if free memory < X1, kill processes with
omm_adj > Y1”
So basically the flow will look like this:
By now, I hope you have the idea that the less memory you consume, the better chance you have to carry out the important stuff.
The second important idea is to understand that the state of Application is essential. So when your app goes to background, and you still want to send Luke into space, you have to use “Services.”
A Service is an application component that can perform long-running operations in the background, and it does not provide a user interface.
There are couple reasons why you should use service:
1. Tell the system that you have a long-running operation and get your process
oom_adj score accordingly
2. One of 4 entry points for the Android Application (
ContentProvider are rest 3 of them).
Service on the separate process
But there is a dark side of using services:
When I wrote my first app, I succeeded to drain battery from 100% to 0% in less than 3 hours. How? Having the service pull data from the server every 3 minutes :)
I was a young inexperienced padawan.
But somehow, 6 years later, there are some other unknown apps that somehow succeed to do the same:
Every developer was doing whatever they wanted in the background without any restrictions. It was just the Wild Wild West with Sith ruling the galaxy with only a few Jedi fighting them back.
But Google has some good rebels to fight back with
Starting with Marshmallow and then followed by Nougat , Doze mode was then introduced:
If you are not familiar with Doze mode, you really should be. :) In a nutshell — after the user turns off the screen of the device, the doze mode kicksin and disables the network, syncs, GPS, alarms and wifi scans. It stays on up until the user turns on the screen or connects to the charger. The idea — to reduce the number of apps doing the unimportant jobs and in doing so, — saves a user’s battery :)
But it felt like a drop of water in the sea and so Google moved even further — starting with Android Oreo (API 26)
startService() method now throws an
IllegalStateException if an app targeting Android 8.0 tries to use that method in a situation when it isn’t permitted to create background services.
It could be easily fixed by never targeting SDK 26. Some “well-known” apps decide to target API 22 because didn’t want to deal with run-time permission.
But there is more is coming:
- August 2018: New apps required to target API level 26 (Android 8.0) or higher.
- November 2018: Updates to existing apps required to target API level 26 or higher.
- 2019 onwards: Each year the targetSdkVersion requirement will advance. Within one year following each Android dessert release, new apps and app updates will need to target the corresponding API level or higher.
Having said all that — (and I am sure you will come to the same conclusion):
The service as we know it today — has deprecated.
It’s no longer allowed to fulfill its primary purpose , namely to execute the long-running task in the background. Therefore it’s no longer usable.
Unless you are not using Service as Foreground service — there is no any reason for using service. If you need to depend on it — you need to use a job.
Part 2: I have a network call. What is out there:
So let’s take an example of a simple networking call that can download a couple kilobytes.
First the most straightforward way (and incorrect one) is to have a separate thread to execute your repository/activity.
Think about login scenario. You user fills in email, password, and clicks on login button. User’s network has poor 3G, and the users walks in the elevator.
While the network call in progress — users gets a phone call.
OkHttp default timeouts pretty big
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
Usually, we put default for three network retries.
Therefore, In worst case scenario: 3 * 30 = 90 seconds.
Now try to answer a question —
Has login Succeeded?
Well, while your app is in the background, the thing is — you don’t know. As we learned, you can’t rely that your process will stay alive to finish a network call, process the response and save the user login information. Not to mention the fact that the user’s phone can go offline and lose internet connectivity.
From a user perspective — “I already insert my email, password and clicked on sign in button. Therefore I am already logged in”.
If not, trust me — users will think a couple bad things of your UX. But it’s not UX problem, it’s technical.
The next step you might think is — ok, so once I will get a callback that the app is going to background, I will switch to Service. But… wait. You can’t!!! :(
JobScheduler comes to the rescue.
It’s scheduled a
Job to start. When the right time comes — the system will start your
MyJobService and execute whatever is in
The idea is good in theory however
JobScheduler is available only from min API 21. But…
JobScheduler in API 21 & 22 has a pretty buggy component.
Meaning that the real minSDK that you can use
JobScheduler starts with 23.
In your minSDK < 23.. Then you have a
Wait. It’s requires Google Play Services!!
Therefore If you are going to use it, you will leave tens of millions users out — all Amazon Fire, Amazon TV and hundreds of Chinese manufacturers:
JobDispatcher probably isn’t a good option… So
AlarmManager? To schedule recurring alarms to fire and check if network call succeeds and then try to execute it again?…
And what if you still want to benefit from old services on pre-O devices and run services to have zero-time latency to execute calls.
JobIntentService might help Jedis to save a galaxy:
It will give you the ability to execute jobs with regular IntentService on SDK lower than 26 and
JobScheduler on SDK ≥ 26, and it’s part of the Support Library.
Aaaaand… It does not help to execute calls ASAP on Android Oreo devices… So we are back to where we started: , manage with an Android version we are running on, execute calls in the background and reschedule them when the app goes background with a proper scheduler, based on the device state.
Gosh… It’s so hard to be a good Jedi who is willing to save the users battery life and provide amazing UX to our users…
Part 3: WorkManager. Just because work should be easy to do.
So there are different solutions for different device states; Android versions and with/without Google Play Services devices. You probably will start to think that you need to implement all this hard work by yourself and combine different solutions based on “what and how.” The good thing is that the folks at the Android framework hear all our complaints — and they decided to rescue us and the whole galaxy from those Sith’s refactoring :)
On the last Google I/O Android framework, the team announced WorkManager:
WorkManager aims to simplify the developer experience by providing a first-class API for system-driven background processing. It is intended for background jobs that should run even if the app is no longer in the foreground. Where possible, it uses JobScheduler or Firebase JobDispatcher to do the work; if your app is in the foreground, it will even try to do the work directly in your process.
Wow! This exactly what we need — a simple wrapper for all those crazy background execution options.
The WorkManager library has several components:
WorkManager — receives work with arguments & constraints and enqueue it.
Worker — have only one method to implement doWork() which is executed on a background thread. It’s the place where all your background tasks should be done. Try to keep it as simple as possible.
WorkRequest — work request specify which Worker enqueued with what Arguments and what are the Constraints for it (e.g., internet, charging ).
WorkResult — Success, Failure, Retry.
Data — Persistable set of key/value pairs which are passed to/from Worker.
First, create new class extending
Worker and implement
WorkManager to execute this work:
WorkManager will take care of rest. It will choose the best schedule to enqueue your work; it will store all arguments, work details and update the status of your job. You even can subscribe using
LiveData to observe the work status:
Underneath, the architecture of WorkManager lib will looks like this:
You create new
Work and specify which
Worker should do the work with what
Arguments under which
WorkManager saves the work in DB using Room and immediately enqueue the
Work. It’s choosing the best possible scheduler (
Executor aka GreedyScheduler,
AlarmManager) and call for
doWork() method. The result published over
LiveData, and the output is available using
Simple as that.
Of course, there is more.
There is a periodic work that you can schedule:
You can chain two or more jobs sequentially:
And mix between of them.
Note: You can’t enqueue chain of periodic and one-time work together.
There are a lot of other things that you can do with
Work, chain works, merge the arguments from one
Work to another. I encourage you to explore the documentation. It has a lot of good examples.
Real life scenario
We need to build an app that is tracking users location every 15 minutes.
First we create a job to get a location. There is no built-in async way of doing tasks, since
doWork() should return
Result.SUCCESS so if you need to have an async way of executing
doWork() (e.g., query for GPS location) you might use
Latch and another mechanism to block execution:
When we finish with location obtaining operation, we are going to release latch and of course to clean all our mess:
Once LocationTracker obtained, we need to upload it to our server — therefore we are going to schedule another onetime work to upload it to server:
And the UploadWork itself:
Looks simple? And actually — it is.
No more complex JobSchedulers/JobDispatcher/Greed Executors boilerplate code. You create a work, schedule it and it’s get done. Simple as that.
Running up in the background becomes more complicated following the will of saving users battery in the past/future Android releases.
Thanks to the Android team, we have a WorkManager which makes dealing with a background much more natural and straightforward.
And the last thing.
How does the phone look with no battery left?
Thanks for reading. If you liked it, please give me your 👏 👏 and share this. I’ll also love to hear your comments and suggestions :) Thanks