Beyond the Framework, using React-Native with Swift and Kotlin
Part 1: Building Native Modules for Business Logic
By: Daniel Friyia, Agile Software Engineer, TribalScale
A few months ago, I started writing blogs with the goal of bringing advanced React-Native topics to a web developer audience. Most React-Native developers, in my experience, come from a web background and shy away when it comes to making parts of the app natively. Although its certainly true we should keep most of our code in TypeScript/JavaScript, there are also things that can’t be done well by relying on React-Native libraries supplied by the community. When these things happen, its an important skill for the React Native developer to write the required module using native code.
I’ve seen a lot of articles online on the subject of React-Native and native code but most of them seem to assume the reader already has some familarity with native development. The Facebook documentation is especially poor because it doesn’t use modern languages like Kotlin and Swift. Worse still, some of the code in their docs is actually deprecated! I really wanted to write something that would make React-Native developers from a web background more comfortable with going in and writing native code. For this purpose, I wrote two articles. This first article will show you how to write business logic and get it to talk to React-Native. The next article in this series will show you how to build UI using native code. You can find the source for this lesson here
🚨 Note: I recommend you do this tutorial on an Intel based Mac. At the time of writing, M1 Macs seem to have a hard time compiling custom native code. I will remove this message when React-Native repairs their build process on M1.
So what are we building?
We are building a really simple app that shows you the date and increases a counter every second.
I know what you are thinking, I could whip this up in seconds in pure React-Native. The point of this article, however, isn’t to show you how to make a counter. Its to illustrate how you can use native Swift and Kotlin code to communicate with React-Native using Promises and Events. This article will give you the basics to build native modules as complex or as simple as you might desire.
Generating the project
As usual, we start by initializing a TypeScript project. You can do this with the following command:
npx react-native init GoingNative --template react-native-template-typescript
Setting up Kotlin
Unfortunately, React-Native for Android was written in Java and doesn't include Kotlin support by default 😅. Lets start with what could be the most tedious part of this process which is setting up Kotlin with React-Native. First make sure you open the android
directory in Android Studio. 🚨 Do not attempt to open the root directory in Android Studio! It won’t allow you to edit the native code properly 🚨. In order to get Kotlin to work we’ll have to make changes to some gradle files in the android
folder of the project. Think of gradle
like you would think about the package.json.
Its essentially a package manager for Android.
We start in the android/build.gradle
file. Please copy and paste the following lines into your buildscript
:
buildscript {
ext {
...
kotlin_version = '1.4.10'
}
...
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Those lines tell gradle which Kotlin version we want to use for this project.
Next, we move to the android/app/build.gradle
file. In this file we want to make the following changes:
...
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'...
dependencies {
...
implementation 'androidx.core:core-ktx:1.3.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
These lines tell Android to install Kotlin and to apply the Kotlin plugin to this project.
If you get lost here you can have a look at my gradle files: build.gradle & app/build.gradle
Calling a native function from React-Native
Android Setup
Since we just got Kotlin up and running and you are probably in Android Studio anyway, lets start by creating our first native function! The first thing you’ll want to do is create a package to hold the code for your native module. To do this right click on the java/com.goingnative
directory and select new > package.
The next thing you’ll want to do is name the package samplenativemodule
.
After that right-click the module and select New > Kotlin File/Class
Name the new Kotlin file SampleModule.
Once inside the file you’ll want to import the react bridge, the SimpleDateFormat, the java utilities and the React Method annotation. It should look something like this:
package com.goingnative.samplenativemodule
import com.facebook.react.bridge.*
import java.text.SimpleDateFormat
import java.util.*import com.facebook.react.bridge.ReactMethod
Next, make a class and call it sample module. We’ll use the Kotlin syntax sugar for a constructor here. In Kotlin you can put the parameters for the contructor in the class declaration and call the super contructor afterwards. It looks like this:
class SampleModule(context: ReactApplicationContext) : ReactContextBaseJavaModule()
{
...
}
Now that you’ve created the class, you’ll probably notice you are receiving a compile error. This is because ReactContextBaseJavaModule
requires that you implement a getName
function. The getName
function is used by React-Native to tell JavaScript what you want to name your native module. Since we are making a clock, let’s get this function to return "Clock"
. It will look like this:
override fun getName(): String {
return "Clock"
}
The last thing we’ll want to do in this class is actually implement the method we want to expose to JavaScript. The method must be annotated with @ReactMethod
so that the React-Native engine knows to mark it as a JS method. The method also takes in a promise and resolves if the method is successful and rejects if it fails. For our simple example, lets just resolve the promise. The method for returning a date will look like this
@RequiresApi(Build.VERSION_CODES.O)
@ReactMethod
fun getCurrentTime(promise: Promise) {
val date = ZonedDateTime.now( ZoneOffset.UTC ).format( DateTimeFormatter.ISO_INSTANT )
promise.resolve(date)
}
Have a look at your file. At this point, it should look something like this:
Now that we’ve written our module, we need to create a package that exposes our module to JavaScript. In your samplenativemodule
package create a new Kotlin file and call it SampleModulePackage
. The contents of this file will be as follows:
In this class createViewManagers
is used if you are creating native views. In our case we are just making modules so we can just return an empty list. For createNativeModules
on the other hand we want to create our module, put it into a list of packages and then return them to the React-Native Android framework.
The last step is to find the getPackages
method in MainApplication.java
. We can’t write this part in Kotlin unfortunately since React-Native pre-generates it in Java. The cool thing though is that Java and Kotlin are 100% interpolatable. This means any class you create in Kotlin can be used in Java as if it was a native Java object. You’ll want to update getPackages
so that it looks like this:
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new SampleModulePackage());
return packages;
}
At this point I recommend you try and compile the app just to make sure you didn’t leave any syntax errors behind. Once you’ve compiled we’ll jump over to iOS and perform this same setup.
iOS Setup
iOS, like Android, has a lot of tedious steps we need to follow before we can call a native method. Lets open up Xcode and get the painful part of this out of the way shall we? 🚨 Like Android, open the iOS
directory! Opening the root directory will create issues 🚨.
The first step we need to do here is create a group for our module lets call it SampleModule.
Next lets create a plain Swift file called SampleModule.swift.
During this process you will be prompted to create a bridging header.
Make sure to click yes on generating this file. The bridging header will be in your group. Make sure to move it up to the root directory. In the bridging header copy and paste these imports. These imports will be used to expose Objective-C methods to Swift.
#import <React/RCTBridgeModule.h>
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
#import <React/RCTEventEmitter.h>
The next step we have to make is creating the module, similar to what we just did in Kotlin. The obvious question arises though, why do we need to expose Objective-C methods to Swift? The answer is that React-Native is written in Objective-C and not Swift. Therefore we need to bridge between the two languages when we want to do any work. To create the module we start by defining the class as follows:
@objc(Clock)
class Clock: RCTEventEmitter {
}
Note the use of the @objc
annotation. This means that this class is compatible with Objective-C. Clock is the Objective-C name for this class. Next we want to override the following required methods:
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}override func constantsToExport() -> [AnyHashable : Any]! {
[:]
}override func supportedEvents() -> [String]! {
return []
}
requiresMainQueueSetup
needs to be set to true so that we can call our native iOS methods in the main thread. constantsToExport
is left blank because there aren’t any constants we need to export to React-Native at this time. The last thing we do is write our method for getCurrentTime.
The method supportedEvents
will be explained a bit later. React-Native provides a resolver and rejecter for this method that you can call similar to the Promise
in Android. To send an ISODate to React-native we write the following method:
@objc func getCurrentTime(
_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) -> Void {
let formatter = ISO8601DateFormatter()
let isoDate = formatter.string(from: Date())
resolve(isoDate)
}
Taken together your entire module should look like this:
The next thing we need to do is expose these methods to React-Native. Unlike Kotlin we don’t use a package, in iOS we create an Objective-C file that exposes the methods. In your sample module package, create a file called SampleModule.m
and make it a plain Objective-C file.
In our new file lets start by importing the bridge:
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
Next, we declare an interface that exposes the module. We write this code to. get started:
@interface RCT_EXTERN_MODULE(Clock, RCTEventEmitter)
@end
Here is where the magic happens. React-Native gives us C style macros that we can call on our method pointers that reveals them in JavaScript.
At this point compile your iOS app to make sure everything works. If that is running well, we can hop over to React-Native
React-Native, TypeScript
Finally, some nice familiar TypeScript that will allow us to breathe a little 🙂. Lets create a file to wrap our native module and call it Clock.tsx. In this file lets import NativeModules
from React-Native as well. Inside this class we’ll call our native method and export the package as a singelton. It should look something like this:
import {NativeModules} from 'react-native';class SampleModule {
getCurrentTime = async () => {
return NativeModules.Clock.getCurrentTime()
};
}const Clock = new SampleModule();
export default Clock;
We can then call it in our view and display the date as follows:
Here is it working in iOS and Android so far:
Listening for Native Events
iOS
So far we’ve setup a basic method call from native code. Calling a simple function isn’t always enough though. Sometimes we want to listen for arbitrary events to come in like Bluetooth or web request results. This section will show you how to set up events. Since you should still be in your XCode from earlier, let’s start in Swift.
The first thing we’ll want to do is come back to the supportedEvents
method we talked about earlier. For every event we want to expose to React-Native we have to list the event name here. In our case lets call this event onTimeUpdated
we change the method as follows:
override func supportedEvents() -> [String]! {
return ["onTimeUpdated"]
}
At the top of the file, lets add two global variables, a timer and a counter to keep track of seconds:
var timer: Timer?
var count: Int = 0
Next lets create a method to start the timer.
@objc func dispatchEventEverySecond() {
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(
timeInterval: 1,
target: self,
selector: #selector(self.onTimeUpdated),
userInfo: nil,
repeats: true)
}
}
DispatchQueue.main.async
block forces our code to run on the UI thread. We want this to happen so that the UI will update when our time changes. You don’t really need to understand scheduled timer. Just trust that it runs the method every 1 second. Finally we create this method which actually sends the data to React-Native
@objc func onTimeUpdated() {
count += 1
sendEvent(withName: "onTimeUpdated", body: ["count": count])
}
sendEvent
is a utility function supplied by the React-Native framework that sends events to JavaScript. All you need to do is specify the same event name you specified earlier and send a map of what you want the React-Native end to work with.
As always, we aren’t done yet. We still need to expose the method to JavaScript. We can do this using the same Macro we used for getCurrentTime. The interface file should look like this now:
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>@interface RCT_EXTERN_MODULE(Clock, RCTEventEmitter)
RCT_EXTERN_METHOD(
getCurrentTime:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(dispatchEventEverySecond)
@end
Android
The first thing we want to do in Android is go to the SampleModule.kt
file. You’ll want to add the following methods:
@ReactMethod
fun addListener(eventName: String?) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
fun removeListeners(count: Int?) {
// Keep: Required for RN built in Event Emitter Calls.
}
Android events have a weird problem where you will get a warning if you don’t create them. These methods don’t really need to do anything other then exist. Next add a counter and handler as global variables so that we can track the seconds. We’ll also add a global variable to get the React-Context. You should be able to alt-enter
to auto import whatever you need here.
private val _mainHandler = Handler(Looper.getMainLooper())
private val rContext: ReactApplicationContext = contextvar secondsCount = 0
Unlike iOS, Android does not provide you with a sendEvent
function and you need to create that for yourself. You can create the method like this:
private fun sendEvent(
reactContext: ReactContext,
eventName: String,
params: WritableMap
) {
reactContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
This method searches the JavaScript modules for the module you are currently working with and gets the React Device Emitter. Emit sends the event to React-Native with the event name you specify and paramters you specify.
Lastly, add this method to send counter events every second:
@ReactMethod
fun dispatchEventEverySecond() {
_mainHandler.post(object : Runnable {
override fun run() {
secondsCount += 1
val event = Arguments.createMap()
event.putInt("count", secondsCount)
sendEvent(
rContext,
"onTimeUpdated",
event
)
_mainHandler.postDelayed(this, 1000)
}
})
}
Like iOS you don’t need to understand the nuances of the handler post function. For now just know that it sends an event every 1 second. Taken together your file should look like the embed below. Please compile here to make sure you didn’t leave behind syntax errors.
React-Native, TypeScript
The last task we have to complete here is using our counter events in TypeScript. In our Clock.tsx,
add the following two methods
dispatchEventEverySecond = () => {
NativeModules.Clock.dispatchEventEverySecond();
};getCurrentTimeEvents = (callback: (time: number) => void): void => {
const clockEvents = new NativeEventEmitter(NativeModules.Clock);
clockEvents.addListener('onTimeUpdated', (time: {count: number}) => {
callback(time.count);
});
};
The first will just call into the method that starts the dispatch. The second listens for the time updated event and calls the callback. Altogether the file should look like this:
In our useEffect in the App.tsx we can also add the following lines to get seconds and show them on the screen:
...
const [seconds, setSeconds] = useState<number>(0);...useEffect(() => {
...
Clock.getCurrentTimeEvents(setSeconds);
Clock.dispatchEventEverySecond();
}, []);...<Text>The seconds count is: {seconds}</Text>
Altogether our new App.tsx should look like this:
Here is a quick GIF of everything working on Android and iOS
Thanks for reading 🎉. I hope this article will be useful as you set up native modules on your own. Check out part 2 where I build native UI with Kotlin and Swift 🙂.
Have questions about using React-Native with Swift and Kotlin? Click here to speak to one of our experts.
Daniel is an Agile Software Developer specializing in Flutter and React-Native. As much as he likes cross platform he is passionate about all types of mobile development including native iOS and Android and is always looking to advance his skill in the mobile development world.
TribalScale is a global innovation firm that helps enterprises adapt and thrive in the digital era. We transform teams and processes, build best-in-class digital products, and create disruptive startups. Learn more about us on our website. Connect with us on Twitter, LinkedIn & Facebook!