我们来看下如何封装原生模块。具体的参考文档,大家参考下面链接:https://reactnative.cn/docs/native-modules-android
我们废话不多说,笔者通过三个案例,来简介如何自定义Android的原生模块给rn端使用。
- Toast模块
- 存储模块
- 网络请求模块
Toast模块
Toast这个东西,只要接触过Android原生开发的都十分熟悉。那么我们如何在rn端调用原生的Toast呢?
首先,我们必须先搭建好reactnative的运行环境,没有运行环境一切免谈。
第一步,我们必须写一个类继承ReactContextBaseJavaModule类。具体如下:
package com.example.demo.reactnative.base.module;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.HashMap;
import java.util.Map;
/**
* Created by brett.li
* on 2022/10/17
*/
public class ToastModule extends ReactContextBaseJavaModule {
private static ReactApplicationContext reactContext;
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext context) {
super(context);
reactContext = context;
}
//1.该方法的名称将作为该模块在rn端的名称
@NonNull
@Override
public String getName() {
return "CustomToast";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
//2.key将作为rn端的常量名
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
return constants;
}
//3.这个方法是自定义的,可以在rn端调用,这种自定义的方法想要在rn端被调用必须遵循以下几点规则:
//· 方法必须是public访问权限,函数返回值必须是void
// 方法必须被ReactMethod注解修饰
@ReactMethod
public void show(String message,int duration){
Log.e("ToastModule","show 方法被调用");
Toast.makeText(getReactApplicationContext(),message,duration).show();
}
}
第二步,ToastModule类必须添加进ReactPackage中。如何添加呢?需要实现ReactPackage接口,具体看下面代码
public class RNBasePackage implements ReactPackage {
//1.原生模块专属方法
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(RNBaseReactModule.getInstance(reactContext).setReactApplicationContext(reactContext));
modules.add(new ToastModule(reactContext));
return modules;
}
//2.原生UI专属方法
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
List<ViewManager> modules = new ArrayList<>();
return modules;
}
}
如果读者是想要原生模块,那么只需要修改createNativeModules方法即可,将自己写的原生模块,例如上述中的ToastModule添加进去即可,而createViewManagers方法,就返回一个空数组就行了。
第三步,将我们上面新建的RNBasePackage类添加到ReactNativeHost类的getPackages方法中,具体做法如下所示:
package com.example.demo;
import android.app.Application;
import android.content.Context;
import com.facebook.soloader.SoLoader;
/**
* Created by Brett.li on 2021/9/21.
*/
public class MyReactApplication extends Application implements ReactApplication {
@Override
public ReactNativeHost getReactNativeHost() {
return new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages= new PackageList(this).getPackages();
packages.add(new RNBasePackage());
return packages;
}
@Nullable
@Override
protected String getBundleAssetName() {
//就是我们打包出来的bundle的名字,不能写错,不然就加载不到bundle
return "main.bundle";//bundle的名字,默认是index.android.bundle
}
@Override
protected String getJSMainModuleName() {
//即打包脚本中--entry-file后面的参数名。不能写错
return "index";
}
};
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
//application中需要做两件事
//1.实现getReactNativeHost接口
//2.添加SoLoader.init(this, /* native exopackage */ false);这句代码
第四步,rn端调用即可
import React from "react";
//1.导入NativeModules
import {NativeModules, Platform, requireNativeComponent, StyleSheet, Text, TouchableOpacity, View} from "react-native";
const styles = StyleSheet.create({
container: {
// flex: 1,
justifyContent: 'center',
alignItems:'center'
},
hello: {
fontSize: 20,
textAlign: 'center',
margin: 10
}
});
interface Props{
test:string
}
interface State{
test:string
}
export class HelloWorld extends React.Component<Props,State> {
render() {
return (
<View style={styles.container}>
<TouchableOpacity onPress={()=>{
//这个CustomToast正是原生ToastModule类中getName方法的返回值,该方法返回什么字符串,这里就是什么
//show方法正是我们在原生中自定义的被ReactMethod修饰方法
//NativeModules.CustomToast.SHORT这个变量正是getConstants方法中,我们写给map的其中一个键,底层会通过这个键,找到对应的值
//这里多加一个小提醒:rn端如果想要调用原生的方法、常量等东西,必须通过NativeModules.xxx.[方法|常量]来调用,否则根本找不到对应的方法。例如,本例中的NativeModules.CustomToast.show和NativeModules.CustomToast.SHORT
NativeModules.CustomToast.show('Brett', NativeModules.CustomToast.SHORT);
}}>
<Text style={styles.hello}>{"setData"}</Text>
</TouchableOpacity>
</View>
);
}
}
通过上面这四步即可实现rn端和原生的调用。其中前3步是原生端的,第四步才是rn端的。
接下来,我们更深入一点,上面这个Toast模块的调用逻辑是rn调用原生,但是是否调用成功rn端是不知道的。如果rn端想要知道原生是否调用成功,那该怎么办呢?更具体的说是:原生方法想要返回一个值给rn,这该如何实现呢?别急,我们看第二个案例。
存储模块
老规矩,我们照抄原生端的三步曲
package com.example.demo.reactnative.base.module;
import android.util.Log;
import androidx.annotation.NonNull;
import com.example.demo.reactnative.utils.SPUtils;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
/**
* Created by brett.li
* on 2022/10/18
*/
public class StorageModule extends ReactContextBaseJavaModule {
private ReactApplicationContext applicationContext = null;
public StorageModule(ReactApplicationContext context) {
super(context);
applicationContext = context;
}
@NonNull
@Override
public String getName() {
return "BaseStorageModule";
}
//这里着重解释下Promise这个参数,这个参数不是给rn端调用的,举个例子
//原生方法为show(String key) ---> rn端调用如下:BaseStorageModule.show(“xxxxx”)
//原生方法为show(String key,Promise promise) ---> rn端调用如下:BaseStorageModule.show(“xxxxx”),原生只传了key这个参数给rn
//原生方法为show() ---> rn端调用如下:BaseStorageModule.show(),原生没有传参数给rn
//原生方法为show(String key,int value) ---> rn端调用如下:BaseStorageModule.show(“xxxxx”,10),原生只传了两个参数给rn
//原生方法为show(String key,int value,Promise promise) ---> rn端调用如下:BaseStorageModule.show(“xxxxx”,10),原生只传了两个参数给rn
@ReactMethod
public void setStringData(String key, String data,Promise promise) {
Log.e("StorageModule", "key is "+key +" ,data is " + data);
SPUtils.saveStringData(applicationContext, key,data);
//通过promise.resolve或者promise.reject将原生端的值发送给rn端
promise.resolve(null);
}
@ReactMethod
public void getStringData(String key ,Promise promise) {
Log.e("StorageModule", "getData");
String data = SPUtils.getStringData(applicationContext,key,"0");
promise.resolve("{test:" + data + "}");
}
}
package com.example.demo.reactnative.base.hook;
import androidx.annotation.NonNull;
import com.example.demo.reactnative.base.module.RNBaseReactModule;
import com.example.demo.reactnative.base.module.StorageModule;
import com.example.demo.reactnative.base.module.ToastModule;
import com.example.demo.reactnative.base.viewManager.MyTextviewManager;
import com.example.demo.reactnative.base.viewManager.MyWebViewManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Created by brett.li
* on 2022/10/17
*/
public class RNBasePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(RNBaseReactModule.getInstance(reactContext).setReactApplicationContext(reactContext));
modules.add(new ToastModule(reactContext));
modules.add(new StorageModule(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
List<ViewManager> modules = new ArrayList<>();
return modules;
}
}
rn端调用如下:
import React from "react";
//1.导入NativeModules
import {NativeModules, Platform, requireNativeComponent, StyleSheet, Text, TouchableOpacity, View} from "react-native";
const styles = StyleSheet.create({
container: {
// flex: 1,
justifyContent: 'center',
alignItems:'center'
},
hello: {
fontSize: 20,
textAlign: 'center',
margin: 10
}
});
interface Props{
test:string
}
interface State{
test:string
}
export class HelloWorld extends React.Component<Props,State> {
//原生中的getStringData方法里面,我们通过promise.resolve("{test:" + data + "}")将“{test:" + data + "}”这串字符串传递给了rn,
//因此NativeModules.BaseStorageModule.getStringData("brett")返回了Promise<String>这种数据类型,如果想要拿到Promise包裹的数据类型,有两种方式,await或者then。
private async getDataFromStorage(key: string): Promise<String>{
const data = await NativeModules.BaseStorageModule.getStringData("brett")//这样我们拿到的数据就是"{test:" + data + "}"
if(data == null) {
return "0"
}
return data
}
private async setDataFromStorage(key:string,value:any):Promise<any>{
return await NativeModules.BaseStorageModule.setStringData(key,value)
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity onPress={()=>{
NativeModules.CustomToast.show('Brett', NativeModules.CustomToast.SHORT);
}}>
<Text style={styles.hello}>{"setData"}</Text>
</TouchableOpacity>
</View>
);
}
}
上述的rn代码中提及到了async-await这个语法机制,不太清楚的同学,参考下面的资料,这里就不展开来讲解了。https://reactnative.cn/docs/native-modules-android#promises
这样,我们便完成了原生给rn的通信。有同学会问:因为我们上面都是rn先通知原生的,然后原生才回复rn。可不可以省掉第一步,直接原生通知rn呢?这样的话,笔者认为直接存数据到本地就行了。在原生页面存一个数据到磁盘中,进入rn页面直接通过存储模块获取该数字,进而改变rn页面的状态。
其实,原生模块最重要的一个应用场景是封装网络通信接口,一般的app开发我们原生都会封装好网络通信模块来访问后端接口,有时候后端的接口需要携带一些公共的请求头字段才能访问成功。如果我们原生以及将其模块化了,那么rn端的通信难道又得要重新再搞一份吗?
这里有一个网络模块封装的案例大家参考下:
@ReactMethod
public void nativePost(ReadableMap httpRequest, Promise promise){
try {
OkHttpClient okHttpClient = new OkHttpClient();
RequestBody body = RequestBody.create(httpRequest.getString("body"), JSON);
Request request = new Request.Builder()
.url(httpRequest.getString("url"))
.post(body)
.build();
String resp = okHttpClient.newCall(request).execute().body().string();
Log.e("BaseNativeManager","resp is "+resp);
promise.resolve(resp);
} catch (Throwable e) {
promise.reject(e);
e.printStackTrace();
}
}
//rn端调用
//获取Promise包裹的数据的第二种方法:then。
//await只能获取resolve回调方法,reject获取不到,如果使用await this.post(),那么异常情况 promise.reject(e)就获取不到了,当然,我们在原生可以不使用 promise.reject(e);而是 使用promise.resolve(e);这样,我们在rn端就需要对饭回来的值做下处理。
this.post().then(resp=>{
//原生通过resolve方法的会回调到这里来
NativeModules.CustomToast.show("resp is : "+resp, NativeModules.CustomToast.SHORT);
this.setState({test:"获取网络数据成功"})
},rej=>{
//原生通过reject方法的会回调到这里来
NativeModules.CustomToast.show("rej is : "+rej, NativeModules.CustomToast.SHORT);
this.setState({test:"获取网络数据失败"})
})
private async post():Promise<String>{
let request={
url:"http://api.m.mtime.cn/PageSubArea/TrailerList.api",
body:""
}
const data = NativeModules.BaseNativeManager.nativePost(request)//注意:这里并没有await,因此data的数据类型是Promise<String>而不是String
return data
}
最后
牢记四步法便可以完成rn与原生模块的通信功能,希望大家多多实践。