Building Native Modules for React Native with Kotlin and Swift

Forget Java and Objective-C. Instead, learn how to communicate with the native layer of your React Native app using Kotlin and Swift, using primitive and complex data types with promises.

Whitespectre
Whitespectre Ideas
11 min readDec 8, 2022

--

by Jefferson Tavares de Pádua, Senior React Native Developer at Whitespectre

You will find that most of the information out there on building native modules does so using Java and Objective-C. Learn a more modern approach using Kotlin and Swift

In this article, you will learn how to:

  • Use Typescript as a starting point to build native modules
  • Build native Android modules using Kotlin in React Native
  • Build native iOS modules using Swift in React Native

A huge amount of libraries that are available for Android and iOS offer a way (either officially or via a community-managed package) to interact with their features using React Native. Today, most of the information out there about creating native modules does so using Java and Objective-C.

Yet Kotlin and Swift are actually more modern approaches to solving that problem, as they offer a less verbose and more developer friendly syntax when compared to their counterparts (e.g: Kotlin data classes vs POJO’s), and are also oftentimes more performant and less resource hungry (e.g: Swift’s lazy initialization).

So in this article, you’ll learn how to create your own native module with Kotlin and Swift. We’ll go beyond the basic steps and specifically cover how to pass primitive and complex data types through the React Native Bridge, and how to deal with Promises on the native Android and iOS layers. Ready? Let’s get started.

What We’re Going to be Building using Typescript, Kotlin and Swift

Our main goal here is to show you how to build a native module to wrap the functionality of a third-party library.

In order to do that we’ll simulate that we’re exposing the functionality of a module that tracks packages given a track id and a unique code from the user.

For obvious reasons, we’re not going to dabble into using Google Maps, GPS locations or anything like that, we’ll simply expose the functionality of what would’ve been a native library. But of course, these are the steps you’ll need to perform if you’re looking for how to integrate a third party native Android/iOS library without native bindings into React Native to use in your project.

As an actual package tracker would need to have some sort of communication with a backend, we’ll use JavaScript Promises to receive data from the native layers written in Kotlin and Swift, and also use JavaScript objects to pass data from React Native to the native Android and iOS layers to perform operations. We’ll be focusing only on the communication between Typescript to Kotlin/Swift and not build any UI related components, after all, that’s the reason why you ended up here, right?

So let’s start coding.

Using Typescript as a Starting Point

When creating a native module, it’s always a good idea to have a Typescript definition of how that module looks like to be used everywhere the module will be requested. For example, our TS definition will look like this:

import { NativeModules } from 'react-native';
export interface TrackPackageRequest {
id: string;
verificationCode: string;
}
export interface TrackPackageResult {
request: TrackPackageRequest;
distance: number;
status: 'moving' | 'delivered';
}
export interface PackageTrackerModule {
track: (request: TrackPackageRequest) => Promise<TrackPackageResult>;
}
export default NativeModules.PackageTrackerModule as PackageTrackerModule;

Let’s go into detail of what’s happening here:

  • The first thing we have is the import where we’re bringing the NativeModules constant from React Native. That constant holds a reference to all the modules that are initialized with your application, and we’ll use it later on.
  • From there, we have the interfaces TrackPackageRequest and TrackPackageResult that define what we should send to the native module and what we can expect back as a response respectively.
  • Then, we have the PackageTrackerModule interface that defines the actual shape of our module, we’ll use that to cast the value of NativeModules.PackageTrackerModule so that we can rip off all of the benefits of Typescript, even when interacting with our native module.

Disclaimer: You have to make sure methods available on the PackageTracker interface have a matching binding on the native layer (essentially a method with the same name, parameters and return type), otherwise you’ll run into problems when calling the module from React Native.

Now that we have the Typescript side sorted out, let’s dive into the native layers to respond to the calls to the track method that we’re exposing here.

Building Native Android Modules using Kotlin in React Native

Ever since Google announced that Kotlin would be officially supported as a development language for Android, its popularity has skyrocketed and the reasoning behind it is pretty clear:

Kotlin is a modern programming language that’s easy to use, has a great standard library (whereas most Android developers using Java are still stuck with Java 6/7) and is fully interoperable with Java right out of the box, therefore Kotlin is the language we’re going to be using for exposing the Android native functionality of our module.

To start off, let’s create a file named PackageTrackerModule.kt and inside of that file we’ll add a class with the same name that extends from ReactContextBaseJavaModule.

This is an important part of creating the link between the native and TS layer, so let me give you more details of what’s happening under the hood here.

When you extend the ReactContextBaseJavaModule class, Android Studio will prompt you to implement a method called getName, please do so and return PackageTrackerModule as a String.

Remember that in our TS file we imported the NativeModules constant and used it to export an inner object called PackageTrackerModule? Well, it’s no coincidence that we’re returning that same value from the getName method here. That’s what React Native will use to identify what functionality needs to be called when we use our methods in the TS layer, so if you’re creating a module for something else, keep an eye on the names being used.

Remember that in our TS file we imported the NativeModules constant and used it to export an inner object called PackageTrackerModule? Well, it’s no coincidence that we’re returning that same value from the getName method here. That’s what React Native will use to identify what functionality needs to be called when we use our methods in the TS layer, so if you’re creating a module for something else, keep an eye on the names being used.

class PackageTrackerModule : ReactContextBaseJavaModule() {
override fun getName(): String {
return "PackageTrackerModule"
}
}

Now that we have our module, let’s create a React Package to associate it with.

Create another file called PackageTrackerReact.kt and then include a class with the same name that extends from ReactPackage. Once you do that, you’ll be prompted to override the implementation of two methods. We’ll use the createNativeModules method to create an instance of the PackageTrackerModule class we created in our previous step.

class PackageTrackerReact : ReactPackage {
override fun createNativeModules(context: ReactApplicationContext): MutableList<NativeModule> {
return mutableListOf(PackageTrackerModule())
}
override fun createViewManagers(context: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
return mutableListOf()
}
}

The last step of the process on the native Android layer is to link our package to our application. To do that, go to your MainApplication file and find a method called getPackages with a list of packages being returned. What we want to do here is to pass an instance of our package so that React Native is aware that it exists and returns it as a prop of NativeModules in the TS layer:

@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new PackageTrackerReact()); // This line
return packages;
}

Linkage is all done now!

From here, we want to start adding functionality to our module, so let’s head back to the PackageTrackerModule class and create our track method:

@ReactMethod
fun track(data: ReadableMap, promise: Promise) {
val id = data.getString("id")
val verificationCode = data.getString("verificationCode")
if(id == null || verificationCode == null) {
promise.reject("PACKAGE_NOT_FOUND", "Id and Verification code didn't match a package")
return
}
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({
val matchingPackage = packages.find { it.id == id && it.verificationCode == verificationCode }
if (matchingPackage == null) {
promise.reject("PACKAGE_NOT_FOUND", "Id and Verification code didn't match a package")
return@postDelayed
}
val response = Arguments.createMap().apply {
putMap("request", data)
putInt("distance", matchingPackage.distance)
putString("status", matchingPackage.status)
}

promise.resolve(response)
}, 5000)
}

Wait! Don’t get scared 😅

Most of this code is not relevant for your own use case. It’s more to mimic the behavior of a third-party package.

Let’s go through the actual relevant parts you’ll be using on a daily basis:

  • First and foremost is the @ReactMethod annotation. That annotation exposes methods that can be called from the React Native, so if you want to create a method that’s meant only for internal usage within the package, don’t include the annotation in that method.
  • Secondly, the method signature: as you can see, it doesn’t look exactly like what we defined in the TS module right? Well, that’s because all the data you send from React Native to the native Android/iOS layers needs to be converted, and on Android your object parameters will be converted to a ReadableMap.

P/S: If you’re sending primitives (e.g: strings, booleans and so on) you can get their values directly by using the String or Boolean type in Kotlin.

It’s also important to note the second parameter in the track method, which is a Promise that we also didn’t define in the TS definition.

That value will actually get injected internally by React Native so you can use it to return something back to the TS layer when you call your native module. You’ll do so by calling the promise.resolve and promise.reject methods whenever your operations are completed with a success or error respectively.

Be careful with passing the promise object around in your module though, because a promise can only be resolved/rejected once, so if you try to resolve/reject the same promise in multiple places it’ll crash your app.

Other than this, the only thing worth mentioning here is the building of the response value using Arguments.createMap(). As I mentioned before, data being passed across the different layers of the application needs to be converted, so we’ll use it to return a map from the native layer (which pretty much maps to a regular JavaScript object on the other side).

With that, our native module is ready to be called from React Native when running on Android devices!

Let’s now see what needs to be done to make this functionality also available on iOS.

Building Native iOS Modules using Swift in React Native

Now it’s time to implement the functionality on the iOS side of our app. We’re going to be using Swift as our main programming language as it offers a more readable syntax than Objective-C. We’ll jump straight into it this time, as all of the things we did on the TS layer before are still useful, and therefore we don’t have to write them again.

As we plan on writing our actual code in Swift, we’ll have to start off by creating an interface in Objective-C to map our module. That’s why I said before that Swift would be our main programming language, because as of right now there’s no way to avoid writing some Objective-C to interact with the native layer for React Native apps. But no worries, we’ll keep things simple. Create a file called PackageTrackerModule.m and then add the following:

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(PackageTrackerModule, NSObject)
RCT_EXTERN_METHOD(track: (NSDictionary)data resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
@end

As you can see, we’re defining our interface by using the RCT_EXTERN_MODULE macro so that React Native is aware of the existence of our module.

Then secondly we use the RC_EXTERN_METHOD macro to register our track method so that it can also be called from the TS layer.

Similar to what happened on the Android layer, we’re also passing parameters in a slightly different way than what we would expect when calling the function from Typescript, so here’s how it works:

NSDictionary is type React Native transforms our TS objects (you can think of it as the ReadableMap we’ve seen on Android), and the RCTPromiseResolveBlock and RCTPromiseRejectBlock are the same as the Promise object on the Android layer, the only difference is that on iOS instead of bundling those into a single object, we’ll have the separate method being injected automatically for us to use.

Next, let’s create the actual implementation of this interface using Swift:

@objc(PackageTrackerModule)
class PackageTrackerModule : NSObject {
@objc
func track(_ data: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {}
}

For the actual implementation of the class, the main thing to consider is the @objc annotation used both in the class as well as on the method. You can think of these as bindings between the interface we defined in our previous class, and the concrete implementation of our Swift code. The parameters are essentially a 1:1 map to what we defined on our interface, so there’s no need to repeat myself here.

If you’re not super experienced with Objective-C though, here’s something to keep an eye on that can easily crash your app.

See the resolver and rejecter prefixes? Those are named parameters for our Objective-C layer, and therefore they have to match exactly with the names defined on our interface (go look back at the Objective-C file and you’ll see they’re the same name), and also the order for which these values are populated is the same as what is defined on the interface, so don’t define it one way on the interface and another on the class, otherwise your app will crash.

Let’s add our implementation to the track method now:

@objc
func track(_ data: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let id = data["id"] as? String, let verificationCode = data["verificationCode"] as? String else {
reject("PACKAGE_NOT_FOUND", "Id and Verification code didn't match a package", nil)

return
}

let matchingPackage = packages.first{ $0.id == id && $0.verificationCode == verificationCode }

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
if let matched = matchingPackage {
resolve(["request": data, "distance": matched.distance, "status": matched.status])
} else {
reject("PACKAGE_NOT_FOUND", "Id and Verification code didn't match a package", nil)
}
}
}

Again, you can ignore most of the code here as it won’t be needed when you’re implementing your own native module.

What’s relevant here is how the communication with the native layer is performed, so let’s get into it:

As I mentioned earlier, the data passed from React Native is received in the form of a NSDictionary and we can use the keys from our object to access individual properties.

The resolve function also expects an NSDictionary with the data you want to pass back to the React Native layer, so we create it with the same data as we did on Android, and the reject function expects an extra parameter of type NSError in comparison to our Android implementation. We’re passing nil as the value for the last parameter of the reject function as the situations above are expected outcomes, but when integrating with a third party library, if an exception happens internally, this is the parameter you’d use to pass the error information back to Typescript.

And… That’s it!

For iOS there’s no need to create a ReactPackage or anything like that, it all gets registered automatically when we use the RCT_EXTERN_MODULE macro to define the interface of our module.

With that, you now have a custom native module that can be called from React Native and interacts directly with the Android and iOS layers of your app. You might also want to create a user interface to see this code in action now, but let’s be honest, this is quite an advanced topic so if you ended up here, you probably already know how to do that.

Either way, we took the time to put together a very very simple interface to show this module in action, so if you want to give it a go, here’s the link for the github repo.

--

--

Whitespectre
Whitespectre Ideas

Posts from our team on the latest in technology, design, and product strategy. See what we’re up to at https://www.whitespectre.com/