Custom Work Manager initialization
And how to implement it with handling one pitfall
About a half year ago Google introduced a new library called Work Manager for handling complex async work in Android. It should spare your from choosing, whether that work should be done with a JobScheduler, Firebase JobDispatcher or Alarm Manager. Work Manager is smart enough to choose it for you. So it acts as Facade for the programmer, giving to him(her) a concise API. That’s why it is a good library to help with image uploading, logs, analytics sending and e.t.c.
I will not cover all procs and cons of this library, because I want to talk about a pitfall that I faced recently working with that library.
The input data is next: we have an application and we need to use WorkManager
in it. We want to start unique periodic job, when application is started and keep it running infinitely with some big period, like 8–10 hours. Need to mention that application uses libraries, that creates an additional process besides an application process. For example, our project used library Yandex Metrica.
So as we read this, we might think “Okey, I just create a PeriodicWorkRequest
in Application.onCreate
and then run it right here”. It is simple and straightforward and would work in an usual situation, when you have one process application. But not here.
In our multiprocess Application (because we have an application process and Yandex Metrica’s process) we will receive next exception:
Process: com.example:Metrica, PID: 12707
java.lang.IllegalStateException: WorkManager is not initialized properly. The most likely cause is that you disabled WorkManagerInitializer in your manifest but forgot to call WorkManager#initialize in your Application#onCreate or a ContentProvider.
at androidx.work.WorkManager.getInstance(WorkManager.java:134)
So why is that? Let’s take a closer look
Work Manager initialization
Aforementioned exception happens because of 2 reasons that present in the project simultaneously:
- By default,
WorkManager
initialized in a separate provider calledWorkManagerInitializer
- We have two-process-application with starting
WorkRequest
inApplication.onCreate
method
As we have multi process application, each process needs to call Application.onCreate
and don’t forget to mention, that ContentProvider
called only once in a main process by default:
android:multiprocess — If the app runs in multiple processes, this attribute determines whether multiple instances of the content provder are created. If true, each of the app's processes has its own content provider object. If false, the app's processes share only one content provider object. The default value is false…
android:process — The name of the process in which the content provider should run. Normally, all components of an application run in the default process created for the application. It has the same name as the application package…
So the situation is the next. Main process starts WorkManagerInitializer
at the beginning, where WorkManager
is initialized. Then Application.onCreate
is called in the main process and so WorkRequest
is started. Everything is good, since WorkRequest
started after initialization of WorkManager
. Then Second process (in our case Yandex Metrica’s process) is started and Application.onCreate
is called for that process (no provider started). So another one WorkRequest
is created, but for WorkManager
that is not initialized in that process. Thus, aforementioned error occurs. Corresponding issue you can find here.
Solution
There are 3 proposals, but I will tell about them briefly, and then will introduce solution, that we using in our application.
❌Proposal 1: Use static flag
First proposal you can actually find in discussion thread of such error in Metrica’s repository:
For now, It possible to detect main process. Calling Application#onCreate() follow ContentProvider creation via ContentProvider#onCreate(). And ContentProvider creates only in related process. So you can make static variable in Application, like IS_MAIN_PROCESS. It should be false by default.
Then you add one line of code to ContentProvider where you initialize WorkManager. The code will assign true to variable in Application.
So, for all process except main it will be false. then you surround launching tasks with condition like
Here is snippet of code that will clear situation:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
//do process independent tasks here
if (IS_MAIN_PROCESS) {
//do main process dependent tasks here
}
}
companion object {
var IS_MAIN_PROCESS = false
}
}
class ActivationContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
MyApplication.IS_MAIN_PROCESS = true
//do you other content provider stuff if needed
}
//implement other content provider methods
}
So for each process there will be it’s own Application
instance and it’s own class declaration, so IS_MAIN_PROCESS
will be true
only for main process. And for others it will be false
, since ActivationContentProvider
will run only in main process.
Though solution is working, I don’t really like it, because you have static variable in the code (read global state flag) that is changed. For me it’s a code smell. Small, justified, but code smell.
❌Proposal 2: Setup Work Manager in Application.onCreate
As the next solution, you can disable WorkManagerInitializer
provider from manifest file and input default initialization line into Application.onCreate
method just before running your tasks:
//Manifest
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="com.gpn.azs.workmanager-init"
android:enabled="false"/>//Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
//...
WorkManager.initialize(this, Configuration.Builder().build())
//run work manager requests
}
}
Although this is working solution, it is not such a good solution in terms of multi process application, since each process will initialize its own instance of WorkManager
and run needed task.
If you have, for example, some unique task that is needed to be run in application, then with this approach you can actually run it twice, and I’m sure it is not what you want. Method enqueuUniquePeriodicWork
can help you to run only one task in fact, since it will have unique identifier, so next time work will not be started or will replace previous work. But still it is not clean.
❌/✔️Proposal 3: Do not run WorkRequest in Application.onCreate
This is not only proposal but also a way to resolve this issue. You just don’t need to execute WorkRequest
in Application.onCreate
method. Instead of that you can run it in first activity, for example. But here we need to be careful. If we have some periodic work to do (like once per day approximately), then we need to run it when application is started, so Application.onCreate
looks pretty logic.
However there is other starting place. ContentProvider
. This leads us to the next solution
✔️Actual Solution
Let’s introduce next class:
class MyWorkManagerInitializer : DummyContentProvider() {
override fun onCreate(): Boolean {
WorkManager.initialize(context!!, Configuration.Builder().build()) //run your tasks here return true
}
}//where
abstract class DummyContentProvider : ContentProvider() {
override fun onCreate() = true
override fun insert(uri: Uri, values: ContentValues?) = null
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?) = null
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) = 0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0
override fun getType(uri: Uri) = null
}
Please, do not pay much attention to DummyContentProvider
, since it is just simple abstract inheritor, which provides dummy realisation of all required methods.
So our MyWorkManagerInitializer
looks pretty similar to WorkManagerInitializer
. Moreover it expands WorkManagerInitializer
with initialization of required tasks, but we can not just extend WorkManagerInitializer
since it has restricted access:
/**
* The {@link ContentProvider} responsible for initializing {@link WorkManagerImpl}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkManagerInitializer extends ContentProvider {
@Override
public boolean onCreate() {
// Initialize WorkManager with the default configuration.
WorkManager.initialize(getContext(), new Configuration.Builder().build());
return true;
} //dummy implementations of all content provider abstract methods
}
Then we need to disable WorkManagerInitializer
and enable our MyWorkManagerInitializer
in manifest file:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="com.sample.workmanager-init"
android:enabled="false"/>
<provider
android:name=".MyWorkManagerInitializer"
android:authorities="com.sample.MyWorkManagerInitializer"/>
And that’s all. This solution is better for my opinion, and I prefer it, because:
- You don’t need to introduce static variables
- You will run you global tasks only once, not one per process
- You will run global task in a global entry point, not in a local activity
Afterwords
Multi-process applications give us additional colors in a development life. :) I hope you liked this small post. Don’t forget to 👏 and click ‘Follow’. Also will be interesting to know your stories in multi-process environment. ;)