Creating an Android native module for React Native

How make a bridge between your React Native App and native SDKs

As a Mobile Software Engineer, I had to implement the connection between a React Native application and a printer by TCP/IP protocol.

The printer’s manufacturer has support for Android, iOS and Xamarin, but nothing on React Native, so the only way we can use the printer’s SDK is by making a native module both for iOS and Android devices. This article focuses on Android.

Creating a new native module library

First, we install a tool to get the basic scaffolding, run this command in the folder that you want to contain the new library:

$ yarn global add create-react-native-module
$ create-react-native-module --platforms android --package-identifier Bridge
$ cd Bridge
$ yarn install

This creates the basic files of the new native module library. Two files are important: BridgeModule: This class will contain all the methods that can be called from javascript code 

BridgePackage: Here we must inform React Native that it must add BridgeModule to the list of native modules

Migrating to Kotlin

The library project will be set up for Java, but I made the decision to migrate to Kotlin for these reasons:

  • We will use Kotlin coroutines to handle IO operations
  • We will use Koin to handle dependency injection

In order to be able to compile Kotlin code, add this to the build.gradle file, and add Koin and coroutines dependency:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven'

buildscript {
ext.kotlin_version = '1.3.72'
ext.koin_version = '2.1.6'
if (project == rootProject) {
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
}
}
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'com.facebook.react:react-native:+'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

After doing this we can start using Kotlin and convert our Java files to Kotlin.

As of June 2020, when converting BridgePackage to Kotlin you will need to remove the override word to the method createJSModules() because the method was deprecated and if you keep the override it won’t compile.

Connecting Javascript and Kotlin code

How can we call Kotlin methods from Javascript? It’s pretty simple – we only have to add the required method to the BridgeModule class annotated with @ReactMethod 

One important thing to note is that this method ALWAYS must return void or unit, the operations are asynchronous, so to inform Javascript code what happened there are two options:

  • Receiving a success callback and failure callback as parameter
  • Receiving a promise as last parameter and resolve or reject it

Personally, I prefer using promises, because it is easier and cleaner from a Javascript perspective, using async / await. You can get a lot more information about this on React Native documentation.

Let’s suppose that we want expose a method for connecting to the printer given an ip and port number, it would look like this:

@ReactMethod
fun connect(ip: String, port: Int, promise: Promise) {
//handle request
}

How do we call this method from Javascript? Like this:

import Bridge from '';

async function connect(){

try{
await Bridge.connect("localhost",8000)
console.log("Connected to printer")
} catch(e){
//there was an error
console.log(e)
}
}

Dependency Injection

Before explaining how we can process the connection to the printer, let’s explain how I managed dependency injection. For this particular case, I decided to use Koin library because:

  • It is a very lightweight library using pure Kotlin code, easy to use and implement
  • The native module is a tiny project with few dependencies

The most common way of configuring Koin is using the startKoin{} extension, where you pass in context and modules resolving app dependencies. For example:  

val appModule = module {
factory { ConnectorImplementation() }
factory { FileSenderImplementation() }
factory { LifecycleEventListenerImpl() }
factory { DiscoveryImplementation(get()) }
factory { get(Context::class.java) }
factory { ImagePrinterImplementation() }
single { Bridge(get(), get(), get(), get(), get(), get()) }
}

startKoin {
androidContext(context)
modules(appModule)
}

This will work well, that is, until you shake the device and click on “reload,” then the app will crash and you will receive a message saying something like Koin Context is already initialized.

This is because, by default, startKoin() initializes KoinApplication on a GlobalContext and when we reload the app it restarts the global context, so this kind of library should handle its own Koin context.

To fix this problem we change the above code for the following:

fun initializeKoin(context: ReactApplicationContext) {
koinApplication = koinApplication {
androidContext(context)
modules(appModule)
}
}

This koinApplication is  a nullable variable inside an Application Singleton object, so before instantiating our BridgeModule we inject all the dependencies this way:

class BridgePackage : ReactPackage, KoinComponent {

private val bridgeModule by inject()

override fun createNativeModules(reactContext: ReactApplicationContext): List {
initializeKoin(reactContext)
return Arrays.asList(bridgeModule)
}

fun createJSModules(): List<Class> {
return listOf()
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}

override fun getKoin(): Koin {
return ApplicationSingleton.koinApplication!!.koin
}
}

It is important to be aware of the following:

  • BridgePackage implements KoinComponent Interface and overrides getKoin() method; here we access the koinApplication created during Koin initialization and we return Koin object. In this case it is acceptable to use !! to force access to the nullable variable. If library dependencies can’t be provided it would not make sense to continue running the app.
  • We inject bridgeModule lazily, so it will be instantiated only when it is required, by inject<BridgeModule>() uses Lazy<> initialization
  • Module is initialized when we make Arrays.asList<NativeModule>(bridgeModule)

Handling operations with coroutines and informing results

So far we have a connect() method and a way of injecting dependencies. How do we process this connection request and how can we tell Javascript code if it was successful or not?

We can delegate this to another object responsible for handling the connection. For instance, we can make a protocol interface like this: 

interface ConnectionProtocol {
fun connect(ip: String, port: Int, promise: Promise)
}

Our bridge module receives an implementation of this interface on its constructor. Also, our module implements this connection interface and we make use of Kotlin delegation pattern:

class BridgeModule(private val reactContext: ReactApplicationContext,
private val connectionProtocol: ConnectionProtocol
) : ReactContextBaseJavaModule(reactContext),
ConnectionProtocol by connectionProtocol

@ReactMethod
override fun connect(ip: String, port: Int, promise: Promise) {
connectionProtocol.connect(ip, port, promise)
}

It would be preferable to avoid overriding this method, but if we add @ReactMethod directly on the ConnectionProtocol, Javascript code can’t access the method.

So, what we are doing here in the connection is an IO operation. This kind of operation can block our current thread – in this case it would be the React Native thread. A good solution is to use coroutines and dispatch each IO operation on a dedicated IO thread with coroutines. For this you can make an extension function:

fun dispatchNewTask(runnable: Runnable) = runBlocking(Dispatchers.IO) {
val job = launch {
runnable.run()
}
ApplicationSingleton.jobs.add(job)
}

Here we launch a new coroutine on IO thread and save the job to be cancelled later if it is needed. 

Now we can code our connect method:

override fun connect(ip: String, port: Int, promise: Promise) {
dispatchNewTask(Runnable {
try {
val printerConnection = TcpConnection(ip, port)
printerConnection.open()
ApplicationSingleton.printerConnection = printerConnection
promise.resolve(ResolveTypes.SUCCESS.value)
} catch (e: Exception) {
promise.reject(e)
}
})
}

Now we create the connection, open it, save it, and with resolve() we are informing Javascript that connection was successful. If during any of this steps an exception occurs, we call promise.reject()

Handling lifecycle events

So far we should be able to connect to the printer on the React Native app, but there is an important thing we are missing: freeing up resources.

For managing lifecycle events, React Native provides interface LifecycleEventListener

You can make a new class implementing this interface:

class LifecycleEventListenerImpl : LifecycleEventListener {

//Clean up all resources
override fun onHostDestroy() {
try {
ApplicationSingleton.apply {
koinApplication?.close()
printerConnection?.close()
}
} finally {
ApplicationSingleton.printerConnection = null
ApplicationSingleton.jobs.forEach {
it.cancel()
}
}
}

override fun onHostPause() {
}

override fun onHostResume() {
}
}

Now we close our Koin context and printer connection (if it is connected), and finally we cancel all the pending jobs still running.

To connect this listener we must register a new lifecycle event listener on our react context:

class BridgeModule(private val reactContext: ReactApplicationContext,
private val connectionProtocol: ConnectionProtocol,
private val lifecycleEventListener: LifecycleEventListener,

) : ReactContextBaseJavaModule(reactContext),
ConnectionProtocol by connectionProtocol,
LifecycleEventListener by lifecycleEventListener {

private lateinit var eventEmitter: DeviceEventManagerModule.RCTDeviceEventEmitter
private var printerStatusListenerJob: Job? = null

override fun initialize() {
super.initialize()
reactContext.addLifecycleEventListener(this)
}

Conclusion

React Native is a great option for making multi-platform mobile applications, but it does not support all the functionality we could need on an app. As in this case, we may need to use a native SDK or perhaps need to run intensive CPU bound operations in multiple threads. We may even need  to use native components not available on React Native, and in addition to all of this, we can do it while implementing native modules with a little more work and, of course, knowledge of each platform.

To see the full code, you may see the repository on github:

https://github.com/fededri/ZebraBridge

Background Image