React Native and Native Modules: The Android SyncAdapter
This article explores creating a SyncAdapter for React Native Android apps by combining Android's SyncAdapter framework with HeadlessJS. SyncAdapter facilitates data sync with web servers, offering scheduled syncs and improved battery performance. The resulting native module, "react-native-sync-adapter," simplifies the setup, showcasing React Native's modular advantage for Android problem-solving.
I came into the need of using a SyncAdapter for an Android app I want to fully convert into React Native. In this app, I already have the native piece of code working and I could not find a library out there that actually exposes such functionality. This is how I ended up writing it by myself.
What is SyncAdapter?
Basically it is a framework that Android provides to synchronize the data between an Android device and web servers. You could design your own, but they always recommended to use their solution since it provides you with several nice features that you cannot implement by yourself. Basically:
- Schedule syncs: If at scheduled time is not possible to sync, the system will run when possible
- The system only runs your data transfer when the device has network connectivity (so no need to check/worry about it)
- Improved battery performance: Your data transfer is scheduled in conjunction with data transfers from other apps. These factors reduce the number of times the system has to switch on the network, which reduces battery usage
- No need to worry about device being restarted so you do not need to listen for ACTION_BOOT_COMPLETED
And there are more advantages!
What is HeadlessJS?
For this native module that I named as <rte-code>react-native-sync-adapter<rte-code>, we are gonna combine the Android SyncAdapter with the HeadlessJS. Let’s start!
Writing native code
I will not go into details in this part but it is a must to use Android Studio. It will provide you with auto-imports and tones of other features. For writing native code, I have basically followed Creating a Sync Adapter training. You can read through the code. For this section, I am actually going to focus on parts of the code that would actually be new for an Android developer (including React Native code interaction).
Our <rte-code>HeadlessService.class<rte-code> is almost the same as the one explained on the docs but with some extra checks:
There are several things worth mentioning here:
- We hardcoded our <rte-code>TASK_ID<rte-code> to be <rte-code>TASK_SYNC_BACKGROUND<rte-code> so you will need to register a task with this exact same name and that is the task our SyncAdapter will run
- We pass <rte-code>null<rte-code> as parameter so the actual JS task will receive <rte-code>null<rte-code> as extra (since we do not need any param)
- Default timeout is set to 5 minutes which should be enough for any task of this kind. It could have been also possible to pass a parameter for the timeout the same as we do with <rte-code>syncInterval<rte-code> and <rte-code>syncFlexTime<rte-code> (see API).
What about the <rte-code>isAppInForeground<rte-code> check? From the docs:
By default, your app will crash if you try to run a task while the app is in the foreground. This is to prevent developers from shooting themselves in the foot by doing a lot of work in a task and slowing the UI. There is a way around this.
Indeed there is a way around this which is taking a look at the source code and passing a specific parameter. But, I prefer to stick into only launching the service when the app is in the background. My workaround would be:
- In your HeadlessJS task, save (for example in the AsyncStorage) the last timestamp where a sync was completed
- When opening the app, check that timestamp, if a sync is required, just make the app to do the job
Another thing worth mentioning, have you spot the <rte-code>stopSelf()<rte-code>? If we actually return just <rte-code>null<rte-code>, it will start a non-sticky Service that will not be stopped (which is bad for memory/battery) so we make sure that we handle this edge case.
Creating the module
Let’s go ahead and create a native module, named <rte-code>SyncBackground<rte-code> that will be responsible for interacting with our <rte-code>SyncAdapter<rte-code>:
Basically we do two things here:
- Override <rte-code>getName<rte-code> so from now on, our Native Module will be called <rte-code>SyncBackground<rte-code>
- Expose (using <rte-code>@ReactMethod<rte-code>), in this case, just the method <rte-code>init<rte-code> that will set up all the sync hassle for us
Now, loading that module is just a matter of adding a new instance of it to the native modules list, as shown below:
So the apps that want to actually use this module will be able to add <rte-code>new SyncBackgroundPackage()<rte-code> just as we do in the example app.
More on the native side, we want our module to act as an Android library so we can use it in other projects, to do so, take a look at the build.gradle file. The only required thing to convert our module into an Android library (so Android won’t generate an apk for it) is to add the following line:
To learn more check Create an Android library.
And voilà! Our library is ready to be consumed.
Android library perks
One requirement to set up a SyncAdapter is to provide unique resources (<rte-code>strings<rte-code> in this case) plus our app launcher icon. If you check the Android library considerations:
Resource merge conflicts: The build tools merge resources from a library module with those of a dependent app module. If a given resource ID is defined in both modules, the resource from the app is used.
Which basically means that if you define a resource in your library named <rte-code>my-library-string-name<rte-code> and define the same resource name in your app, the one from your app will take precedence. We can take advantage of this since our SyncAdapter needs unique values for each app, so in the app that we will be using this library, we should override these values:
As you can see, I prefixed the specific strings from the library with <rte-code>rnsb<rte-code> so they are safer to override (<rte-code>app_name<rte-code> is a common pattern so I left it like that). The launcher icon that is used in the library should be named <rte-code>ic_launcher.png<rte-code> which is also a default pattern.
If you run the example app and in your phone, go to <rte-code>Settings -> Account<rte-code> you will see how the strings and the launcher icon are correctly being overridden:
As you can see, the values that are used are from our app and not from the library. To your question: Yes, I AM A WIZARD.
By the way, notice at the second screenshot, you can select the option Sync Now which will trigger <rte-code>onPerformSync<rte-code> which will trigger your HeadlessJS task so you can try it out without waiting at all.
Exposing a typed API
I find it nice not to expose the Native Module as it is, but wrap it into your own API where you can actually use some typing (Hello Flow!).
Indeed, I could expose this library as:
But as I said, I would like to provide a more JS-like API and provide Flow types so we make sure we use the library correctly. Here is this tiny API:
I use an Object as parameter so it is easy to extend it in the future without breaking changes. Flow will make sure you call the function with correct parameters plus I do some error checking. Therefore, the internal implementation is totally transparent to the user and they do not need to know/care how do I use/call functions in the native (Java) code.
Binding it all together
Our native module is ready to use. You can actually follow the setup instructions in the repository.
Thanks to React Native modular approach, when one solves a problem on Android (that we do not really want to solve ourselves), then you do not need to do it by yourself never again. Which is nice, isn’t it?
Let me know what you think in the comments below and if you would like me to continue writing about Native Modules even if they are simple examples.