React Native Made Easy Ep. 2 – Native Bridge

Search This thread

Freemind R

Official Huawei Rep
Apr 14, 2020
383
77
Introduction

React Native is a convenient tool for cross platform development, and though it has become more and more powerful through the updates, there are limits to it, for example its capability to interact with and using the native components. Bridging native code with Javascript is one of the most popular and effective ways to solve the problem. Best of both worlds!

Currently not all HMS Kits has official RN support yet, this article will walk you through how to create android native bridge to connect your RN app with HMS Kits, and Scan Kit will be used as an example here.

The tutorial is based on https://github.com/clementf-hw/rn_integration_demo/tree/4b2262aa2110041f80cb41ebd7caa1590a48528a, you can find more details about the sample project in this article: https://forums.developer.huawei.com...d=0201230857831870061&fid=0101187876626530001.

Prerequisites

Basic Android development

Basic React Native development

These areas have been covered immensely already on RN’s official site, this forum and other sources

HMS properly configured

You can also reference the above article for this matter

Major dependencies

RN Version: 0.62.2 (released on 9th April, 2020)

Gradle Version: 5.6.4

Gradle Plugin Version: 3.6.1

agcp: 1.2.1.301

This tutorial is broken into 3 parts:

Pt. 1: Create a simple native UI component as intro and warm up

Pt. 2: Bridging HMS Scan Kit into React Native

Pt. 3: Make Scan Kit into a stand alone React Native Module that you can import into other projects or even upload to npm.

Bridging HMS Scan Kit

Now we have some fundamental knowledge on how to bridge, let’s bridge something meaningful. We will bridge the Scan Kit Default View as a QR Code Scanner, and also learn how to communicate from Native side to React Native side.

First, we’ll have to configure the project following the guide to set Scan Kit up on the native side: https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/scan-preparation-4

Put agconnect-service.json in place

Add to allprojects > repositories in root level build.gradle

Code:
allprojects {
   repositories {
       google()
       jcenter()
       maven {url 'https://developer.huawei.com/repo/'}
   }
}

Add to buildscript > repositories

Code:
buildscript {
   repositories {
       google()
       jcenter()
       maven {url 'https://developer.huawei.com/repo/'}
   }
}

Add to buildscript > dependencies

Code:
buildscript{
   dependencies {
        classpath 'com.huawei.agconnect:agcp:1.2.1.301'
   }
}

Go to app/build.gradle and add this to header

Code:
apply plugin: 'com.huawei.agconnect'

Add this to dependencies

Code:
dependencies {
    implementation 'com.huawei.hms:scanplus:1.1.3.300'
}

Add in proguard-rules.pro

Code:
-ignorewarnings
-keepattributes *Annotation* 
-keepattributes Exceptions 
-keepattributes InnerClasses 
-keepattributes Signature 
-keepattributes SourceFile,LineNumberTable 
-keep class com.hianalytics.android.**{*;} 
-keep class com.huawei.**{*;}

Now do a gradle sync. Also you can try to build and run the app to see if everything’s ok even though we have not done any actual development yet.

Add these to AndroidManifest.xml

Code:
<uses-permission android:name="android.permission.CAMERA" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 <uses-feature android:name="android.hardware.camera" />
 <uses-feature android:name="android.hardware.camera.autofocus" /> 
 <application
    …       
    <activity android:name="com.huawei.hms.hmsscankit.ScanKitActivity" />
 </application>

So the basic setup/configuration is done. Similar to the warm up, we will create a Module file first. Note that for the sake of variance and wider adaptability of the end product, this time we’ll make it a plain Native Module instead of Native UI Component.

Code:
package com.cfdemo.d001rn;
 import androidx.annotation.NonNull;
  
 import com.facebook.react.bridge.ReactApplicationContext;
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
  
  
 public class ReactNativeHmsScanModule extends ReactContextBaseJavaModule {
     private static final String REACT_CLASS = "ReactNativeHmsScan";
     private static ReactApplicationContext reactContext;
  
     public ReactNativeHmsScanModule(ReactApplicationContext context) {
         super(context);
         reactContext = context;
     }
  
     @NonNull
     @Override
     public String getName() {
         return REACT_CLASS;
     }
      
 }

We have seen how data flows from RN to native in the warm up (e.g. @reactProp of our button), There are also several ways for data to flow from native to RN. In Scan Kit, it utilizes startActivityForResult, therefore we need to implement its subsequent listeners.

Code:
package com.cfdemo.d001rn;
 import android.app.Activity;
 import android.content.Intent;
  
 import androidx.annotation.NonNull;
  
 import com.facebook.react.bridge.ActivityEventListener;
 import com.facebook.react.bridge.BaseActivityEventListener;
 import com.facebook.react.bridge.ReactApplicationContext;
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
  
 public class ReactNativeHmsScanModule extends ReactContextBaseJavaModule {
     private static final String REACT_CLASS = "ReactNativeHmsScan";
     private static ReactApplicationContext reactContext;
  
     public ReactNativeHmsScanModule(ReactApplicationContext context) {
         super(context);
         reactContext = context;
         reactContext.addActivityEventListener(mActivityEventListener);
     }
  
     @NonNull
     @Override
     public String getName() {
         return REACT_CLASS;
     }
  
     private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
         @Override
         public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
  
         }
     };
 }

There are couple small details we’ll need to add. First, React Native javascript side expects a Promise from the result.

Code:
private Promise mScannerPromise;

We also need to add a request code to identify that this is our scan kit activity. 567 here is just an example, the value is of your own discretion

Code:
private static final int REQUEST_CODE_SCAN = 567

There will be several error/reject conditions, let’s identify and declare their code first

Code:
 private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
    private static final String E_SCANNER_CANCELLED = "E_SCANNER_CANCELLED";
    private static final String E_FAILED_TO_SHOW_SCANNER = "E_FAILED_TO_SHOW_SCANNER";
    private static final String E_INVALID_CODE = "E_INVALID_CODE";

At this moment, the module should look like this

Code:
package com.cfdemo.d001rn;
 import android.app.Activity;
 import android.content.Intent;
  
 import androidx.annotation.NonNull;
  
 import com.facebook.react.bridge.ActivityEventListener;
 import com.facebook.react.bridge.BaseActivityEventListener;
 import com.facebook.react.bridge.Promise;
 import com.facebook.react.bridge.ReactApplicationContext;
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
  
 public class ReactNativeHmsScanModule extends ReactContextBaseJavaModule {
     private static final String REACT_CLASS = "ReactNativeHmsScan";
     private static ReactApplicationContext reactContext;
     private Promise mScannerPromise;
     private static final int REQUEST_CODE_SCAN = 567;
     private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
     private static final String E_SCANNER_CANCELLED = "E_SCANNER_CANCELLED";
     private static final String E_FAILED_TO_SHOW_SCANNER = "E_FAILED_TO_SHOW_SCANNER";
     private static final String E_INVALID_CODE = "E_INVALID_CODE";
  
     public ReactNativeHmsScanModule(ReactApplicationContext context) {
         super(context);
         reactContext = context;
         reactContext.addActivityEventListener(mActivityEventListener);
     }
  
     @NonNull
     @Override
     public String getName() {
         return REACT_CLASS;
     }
  
     private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
         @Override
         public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
              
         }
     };
 }

Now let’s implement the listener method

Code:
if (requestCode == REQUEST_CODE_SCAN) {
     if (mScannerPromise != null) {
         if (resultCode == Activity.RESULT_CANCELED) {
             mScannerPromise.reject(E_SCANNER_CANCELLED, "Scanner was cancelled");
         } else if (resultCode == Activity.RESULT_OK) {
             Object obj = intent.getParcelableExtra(ScanUtil.RESULT);
             if (obj instanceof HmsScan) {
                 if (!TextUtils.isEmpty(((HmsScan) obj).getOriginalValue())) {
                     mScannerPromise.resolve(((HmsScan) obj).getOriginalValue().toString());
                 } else {
                     mScannerPromise.reject(E_INVALID_CODE, "Invalid Code");
                 }
                 return;
             }
         }
     }
 }

Let’s walk through what this does

When the listener receives an activity result, it checks if this is our request by checking the request code.

Afterwards, it checks if the promise object is null. We will cover the promise object later, but briefly speaking this is passed from RN to native, and we rely on it to send the data back to RN.

Then, if the result is a CANCELED situation, we tell RN that the scanner is canceled, for example closed by user, by calling promise.reject()

If the result indicates OK, we’ll get the data by calling getParcelableExtra()

Now we’ll see if the resulting data matches our data type and is not empty, and then we’ll call promise.resolve()

Otherwise we will resolve a general rejection message. Of course here you can expand and give a more detailed breakdown and resolution if you wish

This is a lot of checking and validation, but one can never be too safe, right?

Cool, now we have finished the listener, let’s work on the caller! This is the method we’ll be calling in RN side, indicated by the @reactMethod annotation.

[CODE @reactMethod
public void startScan(final Promise promise) {

} [/CODE]

Give it some content

[CODE @reactMethod
public void startScan(final Promise promise) {
Activity currentActivity = getCurrentActivity();

if (currentActivity == null) {
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist");
return;
}

// Store the promise to resolve/reject when picker returns data
mScannerPromise = promise;

try {
ScanUtil.startScan(currentActivity, REQUEST_CODE_SCAN, new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.ALL_SCAN_TYPE).create());
} catch (Exception e) {
mScannerPromise.reject(E_FAILED_TO_SHOW_SCANNER, e);
mScannerPromise = null;
}
}[/CODE]

Let’s do a walk through again

First we get the current activity reference and check if it is valid

Then we take the input promise and assign it to mScannerPromise which we declared earlier, so we can refer and use it throughout the process

Now we call the Scan Kit! This part is same as a normal android implementation.

Of course we wrap it with a try-catch for safety purposes

At this point we have finished the Module, same as the warm up we’ll need to create a Package. This time it is a Native Module therefore we register it in createNativeModules() and also give createViewManagers() an empty list.

Code:
package com.cfdemo.d001rn;
  
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
  
 import com.facebook.react.ReactPackage;
 import com.facebook.react.bridge.NativeModule;
 import com.facebook.react.bridge.ReactApplicationContext;
 import com.facebook.react.uimanager.ViewManager;
  
 public class ReactNativeHmsScanPackage implements ReactPackage {
     @Override
     public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
         return Arrays.<NativeModule>asList(new ReactNativeHmsScanModule(reactContext));
     }
  
     @Override
     public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
         return Collections.emptyList();
     }
 }

Same as before, we’ll add the package to our MainApplication.java, import the Package, and add it in the getPackages() function

Code:
import com.cfdemo.d001rn.ReactNativeWarmUpPackage;
 import com.cfdemo.d001rn.ReactNativeHmsScanPackage;
  
 public class MainApplication extends Application implements ReactApplication {
         ...
         @Override
         protected List<ReactPackage> getPackages() {
           @SuppressWarnings("UnnecessaryLocalVariable")
           List<ReactPackage> packages = new PackageList(this).getPackages();
           // Packages that cannot be autolinked yet can be added manually here, for example:
           // packages.add(new MyReactNativePackage());
           packages.add(new ReactNativeWarmUpPackage());
           packages.add(new ReactNativeHmsScanPackage());
           return packages;
         }

All set! Let’s head back to RN side. This is our app from the warm up exercise(with a bit style change for the things we are going to add)

2640852000005943868.20200521092540.56116845221370356783470154751674:50510624055439:2800:50CC907FB510A562F1D9E858A3896D3F14436225267EA1F9F9B88E2B6C7CDE96.png


Let’s add a Button and set its onPress property as this.onScan() which we’ll implement after this

Code:
render() {
    const { displayText, region } = this.state
    return (
      <View style={styles.container}>
        <Text style={styles.textBox}>
          {displayText}
        </Text>
        <RNWarmUpView
          style={styles.nativeModule}
          text={"Render in Javascript"}
        />
        <Button
          style={styles.button}
          title={'Scan Button'}
          onPress={() => this.onScan()}
        />
        <MapView
          style={styles.map}
          region={region}
          showCompass={true}
          showsUserLocation={true}
          showsMyLocationButton={true}
          >
        </MapView>
      </View>
    );
  }

Reload and see the button

2640852000005943868.20200521092733.81763351174921058258889745773695:50510624055439:2800:FA3CE3EFE0AC945801202F17D6CEABC33A55369EFD3A39D14F90CE0BD8367976.png


Similar to the one in the warm up, we can declare the Native Module using this simple way

Code:
const RNWarmUpView = requireNativeComponent('RCTWarmUpView')
const RNHMSScan = NativeModules.ReactNativeHmsScan

Now we’ll implement onScan() which uses the async/await syntax for asynchronous coding

Code:
  async onScan() {
    try {
      const data = await RNHMSScan.startScan();
      // handle your data here
    } catch (e) {
      console.log(e);
    }
  }

Important! Scan Kit requires CAMERA and READ_EXTERNAL_STORAGE permissions to function, make sure you have handled this beforehand. One of the recommended way to handle it is to use react-native-permissions library https://github.com/react-native-community/react-native-permissions. I will make another article regarding this topic, but for now you can refer to https://github.com/clementf-hw/rn_integration_demo if you are in need.

Now we click…TADA!

2640852000005943868.20200521093127.56566239173183902642451994517101:50510624055439:2800:C6BC4F6FFD7ABA4F1FDE578CBC00E24A03025D06F1FD65F151B47533757040D5.png


In this demo, this is what onScan() contains

Code:
async onScan() {
  try {
    const data = await RNHMSScan.startScan();
    const qrcodeData = {
      message: (JSON.parse(data)).message,
      location: (JSON.parse(data)).location,
      my_location: (JSON.parse(data)).my_location
    }
    this.handleData(qrcodeData)
  } catch (e) {
    console.log(e);
  }
}

Note: one minor modification is needed if you are basing on the branch of this demo project mentioned before

Code:
onLocationReceived(locationData) {
    const location = typeof locationData === "object" ? locationData : JSON.parse(locationData)
      …

Now let’s try scan this

2640852000005943868.20200521093752.61195028371605821096147234551618:50510624055439:2800:299C461E0B9DFAE2BF40417F00C2CAD209EF1DB71C9E40C2A40E126D48609452.png


The actual data contained in the QR Code is

Code:
{"message": "Auckland", "location": {"lat": "-36.848461","lng": "174.763336"}}

Which bring us to Auckland!

2640852000005943868.20200521094000.98177792328752176093651396108876:50510624055439:2800:06E89A3F29AF026026726163B91DE6F028F6B776C6089C5451BB5D0430E3469F.png


Now your HMS Scan Kit in React Native is up and running!

Pt. 2 of this tutorial is done, please feel free to ask questions. You can also check out the repo of the sample project on github: https://github.com/clementf-hw/rn_integration_demo, and raise issue if you have any question or any update.

In the 3rd and final part of this tutorial, we'll go through how to make this RN HMS Scan Kit Bridge a standalone, downloadable and importable React Native Module, which you can use in multiple projects instead of creating the Native Module one by one, and you can even upload it to NPM to share with other fellow developers.