Domestic developers of React Native are mostly Web front-end, and most of them have no knowledge of Native development, so some functions related to Native are difficult to implement.

Recently, I had to package an SDK for React Native, so I studied it carefully. In fact, it is not complicated to write the Native module.

This article briefly records the process of writing Native modules, releasing them to NPM and using them in main projects. You need to be familiar with React Native and its directory structure.

Project creation

The native module project was a bit of a hassle, and instead of digging into how to build the project, I used a good scaffolding project that could create a template project for us directly.

This article uses the default react-native awesome-module name.

npx create-react-native-library react-native-awesome-module
Copy the code

Enter the corresponding information

The languages chosen here are Kotlin & Swift because they are more friendly to TypeScript developers.

Choose Native Module (to Expose Native APIs) as the next type.

Then perform the YARN installation dependency as prompted.

The directory structure

.├ ── CONTRIBUTING. Md ├── Android ├── Bass.config.js ├── ─ Bass.js ├── ─ Customization React-native awesome module. Podspec ├── SRC ├─ ├─ tsconfig.buildCopy the code

Just focus on the following directories

  • SRC — NPM publish, other projects will reference this file, here will do some native method export, type definition.

  • Ios — React Native iS a file that will load on the ios app. We will use Swift to write this part

  • Android — React Native is a file that will load on the Android terminal. We will use Kotlin for this part

  • Example – Run the example project to debug our native module

The React Native version of Example generated by this template is 0.63.4. You need to modify a bit of code to run the Example iOS app.

Add example/ios to the Podfile file

use_flipper! ({'Flipper' => '0.80.0'})

Modified to

use_flipper! ({' Flipper - Folly '= >' 2.5.3 ', 'Flipper' = > '0.87.0', 'Flipper - RSocket' = > '1.3.1})

Introduction of grammar

A quick example of TypeScript syntax

let

  • Swift => var
  • Kotlin => var

const

  • Swift => let
  • Kotlin => val

function

  • Swift => func
  • Kotlin => fun

object

  • Swift => [String: Any]()
  • Kotlin => Map

const array:string[] = []

  • Swift => let array = [String]()
  • Kotlin => val a: Array<String> = []

The iOS side

For iOS, the podSpec file is in the project root directory, where we need to write our module’s dependencies.

require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
  s.name         = "react-native-awesome-module"
  s.version      = package["version"]
  s.summary      = package["description"]
  s.homepage     = package["homepage"]
  s.license      = package["license"]
  s.authors      = package["author"]

  s.platforms    = { :ios= >"10.0" }
  s.source       = { :git= >"https://github.com/zhangyu1818/react-native-awesome-module.git".:tag= >"#{s.version}" }

  s.source_files = "ios/**/*.{h,m,mm,swift}"

  s.dependency "React-Core"
end
Copy the code

S. platforms represents the lowest iOS version of this module; if the main project version is lower than the version of this file, the module cannot be used.

S.dependency indicates the Pod package that this module depends on. Additional Pod packages need to be added if we want to rely on them.

For example, if I want to rely on GooglePlaces, I need to add the following.

s.dependency "GooglePlaces"."~ > 4.2.0"
Copy the code

Since GooglePlaces is only available on iOS 10 up to version 4.2.0, I need to mark the version.

Next we look at the iOS module file.

Writing iOS modules

It is recommended to read the official iOS module documentation before doing so.

Double-click the ios/AwesomeModule xcodeproj using Xcode open the project.

Next, look at the table of contents.

├── Heavy Exercises, ├─ heavy Exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercisesCopy the code
  • Awesomemodule-bridge-header — This file is a bridge file for Objective-C and Swift. We need this file because we’re writing in Swift, and usually we don’t need to change this file.

  • AwesomeModule. M – We need to declare the modules and methods to be exported in this file.

  • AwesomeModule. Swift – We need to write the implementation of the method in this file.

AwesomeModule.m

The syntax in this file is objective-C syntax, which is hard to understand at first sight.

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(AwesomeModule, NSObject)

RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b
                 withResolver:(RCTPromiseResolveBlock)resolve
                 withRejecter:(RCTPromiseRejectBlock)reject)

@end
Copy the code

RCT_EXTERN_MODULE is a macro in OC that exports our modules and methods.

@interface RCT_EXTERN_MODULE(AwesomeModule, NSObject)
Copy the code

The AwesomeModule in this parameter is our module name.

RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b
                 withResolver:(RCTPromiseResolveBlock)resolve
                 withRejecter:(RCTPromiseRejectBlock)reject);
Copy the code

This is where a native method called multiply is derived to compute the product result.

  • (float) A represents the first unknown parameter of type float

  • WithB :(float)b represents the second parameter named withB, variable b, and type float.

  • WithResolver :(RCTPromiseResolveBlock)resolve represents the resolve callback of the Promise in JS.

  • WithRejecter (RCTPromiseRejectBlock) Reject) represents the Reject callback of a Promise in JS.

Because the parameters passed in Objective-C have names, they look a little bit weird, so here’s an example of pseudocode.

multiply(10, withB: 20)
Copy the code
AwesomeModule.swift

The Swift code here, at least, is readable without learning it.

@objc(AwesomeModule)
class AwesomeModule: NSObject {
    @objc func multiply(a: Float.b: Float.resolve:RCTPromiseResolveBlock.reject:RCTPromiseRejectBlock){
        resolve(a*b)
    }
}
Copy the code

The AwesomeModule class inherits NSObject with a multiply method that returns no value and has four arguments that need to be added to @objc as it is passed to the OC call.

Add a new method

For example, if we want to add a new method, this should be called on the JS side.

queryPlace("Chengdu", {filter: "city",
})
.then(result= >{
  console.log(result);
})
.catch(error= >{
  console.log(error);
})
Copy the code

Add a definition

// AwesomeModule.m
@interface RCT_EXTERN_MODULE(AwesomeModule, NSObject)
// ...
RCT_EXTERN_METHOD(queryPlace: (NSString *)query
                  options:(NSDictionary *)options
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject);
@end
Copy the code

Implementation method

This is just a simple example, and the real scenario is definitely calling methods of other packages.

// AwesomeModule.swift

// ...

@objc func queryPlace(_ query: String.options: [String: Any].resolve: @escaping RCTPromiseResolveBlock.reject: @escaping RCTPromiseRejectBlock){
  let filter = options["filter"] as? String
  DispatchQueue.main.sync{
    if let filter = filter {
      resolve(query + filter)
    }else {
      let error = NSError(domain: "error", code: 500, userInfo: nil)
      reject("Something went wrong."."No filter passed in", error)
    }
  }
}
Copy the code

In this method, if filter is passed in, resolve returns query + filter, otherwise reject throws an error.

This method uses dispatchqueue.main. async to start a task on the main line of iOS that calls resolve.

What we call a callback is called a closure in iOS, and @escaping says resolve is an escape closure.

In JS, if function A returns function B, and function B uses A variable from function A, it automatically closes. IOS needs to maintain ownership of that variable with @escaping.

Additional methods need to be implemented

In iOS, its layout UIKit runs on the main thread, and our React Native runs on another thread, so you can’t manipulate the UIKit on the main thread. So we usually call dispatchqueue.main. sync or dispatchqueue.main.async to perform our operation.

So we need to implement additional methods to tell React Native which thread the module should run on.

// AwesomeModule.swift

// ...

@objc var methodQueue = DispatchQueue.main

@objc static func requiresMainQueueSetup(a) -> Bool {
  return true
}
Copy the code

If our module does not need to be initialized on the main thread, we need to return false for requiresMainQueueSetup, and we do not need the methodQueue attribute.

The Android end

Android terminal compared to iOS terminal will be much simpler, no OC archaic syntax, no declaration file, directly write on the line.

On Android, if our module has additional dependencies, we can write them in Android /build.gradle.

Again, take the example of adding GooglePlaces.

dependencies {
  // noinspection GradleDynamicVersion
  api 'com.facebook.react:react-native:+'
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

  implementation 'com. Google. Android. Libraries. Places: places: 2.5.0' // Plus this row
}
Copy the code

The same SDK may have different package names and versions on Android and iOS

Writing Android modules

The SRC └ ─ ─ the main ├ ─ ─ AndroidManifest. XML └ ─ ─ Java └ ─ ─ com └ ─ ─ reactnativeawesomemodule ├ ─ ─ AwesomeModuleModule. Kt └ ─ ─ AwesomeModulePackage.ktCopy the code

We just need to focus on the awesomemoduleModule. kt file.

AwesomeModuleModule.kt

class AwesomeModuleModule(reactContext: ReactApplicationContext) :
  ReactContextBaseJavaModule(reactContext) {

  override fun getName(a): String {
    return "AwesomeModule"
  }

  @ReactMethod
  fun multiply(a: Int, b: Int, promise: Promise) {
    promise.resolve(a * b)
  }
}
Copy the code

Just add an @reactMethod annotation to the method you want to export.

Or the JS method that needs to be added above as an example.

@ReactMethod
fun queryPlace(query: String, options: ReadableMap, promise: Promise) {
  val filter = options.getString("filter")
  if(filter ! =null) {
    promise.resolve(query + filter)
  } else {
    promise.reject("error"."No filter passed in")}}Copy the code

Isn’t it easy?

Conversion of values to each other

The iOS, Android, and React Native conversions require us to do some things ourselves.

React Native transmits values to Native

Values have the following structure

const options = {
	name: "zhangyu1818".coordinate: {
    latitude: 30.656994.longitude: 104.080009}}Copy the code

iOS

func test(options: [String:Any]){
  if let name = options["name"] as? String {
    / / operation name
  }
  if let coordinate = as? [String: [String: Double]] {
    if let latitude = coordinate["latitude"].let longitude = coordinate["longitude"] {
      // Operate latitude and longitude}}}Copy the code

Android

fun test(options: ReadableMap){
  val name = options.getString("filter")
  val coordinate = options.getMap("coordinate")
  
  vallatitude = coordinate? .getDouble("latitude")
  vallongitude = coordinate? .getDouble("longitude")}Copy the code

The React Native passes the value to React Native

A simple example

Native objects with the following structure (TypeScript only for type definition)

interface Result {
  name: string
  types: string[]
  complex: { values? :string[]
    address: {
      text: stringcoordinate? : {latitude: number
        longitude: number}}}}Copy the code

For your information, this is not just a Dictionary or Map.

iOS

func convert(value: Result)- > [String: Any] {
  let dic:[String:Any] = [
    "name": value.name,
    "types": value.complex.values,
    "complex": [
      "values": value.complex.values,
      "address": [
        "text": value.complex.address.text,
        "coordinate": [
          "latitude": value.complex.address.coordinate?.latitude,
          "longitude": value.complex.address.coordinate?.longitude
        ]
      ]
    ]
  ]

  return dic
}
Copy the code

Android

Android should use Arguments, WritableMap, etc provided by React Native.

Turn an array with the Arguments. MakeNativeArray, turn the Map with the Arguments. MakeNativeMap

fun convert(value: Result): WritableMap {
  return Arguments.makeNativeMap(
    mapOf(
      "name" to value.name,
      "types" to value.types,
      "complex" to mapOf(
        "values" to value.complex.values,
        "address" to mapOf(
          "text" to value.complex.address.text,
          "coordinate" to mapOf(
            "latitude" to value.complex.address.coordinate?.latitude,
            "longitude" to value.complex.address.coordinate?.longitude
          )
        )
      )
    )
  )
}
Copy the code

conclusion

The overall is relatively simple, because I also started from scratch to spend a few days to make one, but the company does not allow open source, can not be sent to everyone for reference.

After careful consideration, although I have been learning in Swift intermittently for one year, the development module does not need the Native basis, which is basically to call the Native SDK method and expose it to React Native.

Just look at the Swift documentation, the Kotlin documentation, and get a quick look at the syntax.

If you need to write a package for a native view, it’s more complicated and requires some native ability, which I haven’t covered yet.


On the first day of the Spring Festival holiday, I wrote for 2 hours without playing games.

Spring Festival, finally can have a good rest! Happy Spring Festival to you all!