React Native-原生模块开发
jeffrey1995 opened this issue · 0 comments
前言:
所谓原生模块开发,很重要的就是Android/iOS原生代码和ReactNative代码的交互,以及在ReactNative代码中使用原生的组件。下面将从这两个地方开始讲解(示例以Android平台为基础):
先回顾一下RN组件的生命周期:
constructor():
在组件被加载前最先调用,第一个语句必须是super(props)。
componentWillMount():
在初始渲染(render函数被RN框架执行之前)前被执行。在函数中调用setState函数改变了某些状态机变量的值,RN框架不会立马执行渲染操作,而是等该函数执行完之后执行初始渲染。
componentDidMount():
执行完初始渲染之后立马被调用。将从网络侧请求数据的代码放在这里比较合适。
componentWillReceiveProps(nextProps):
RN初始化渲染执行完成后,当RN组件收到新的props时,这个函数被调用。函数接受的参数是一个object为新的props。
shouldComponentUpdate(nextProps, nextState):
RN初始化渲染执行完成后,当RN组件接收到新的state或者props时,该函数被调用。接受两个参数:第一个是新的props,第二个是新的state。该函数返回一个布尔类型的值,表示RN框架针对此次改变是否重新渲染该组件。
componentWillUpdate(nextProps, nextState):
RN组件在初始化渲染执行完成之后,RN框架在重新渲染RN组件前会调用这个函数。
componentDidUpdate(prevProps, prevState):
RN组件在初始化渲染执行完成之后,RN框架在重新渲染RN组件完成后会调用这个函数,传入的两个参数是渲染前的Props和state。
componentWillUnmount():
在RN组件被卸载前,这个函数被执行。
一、原生代码和RN代码的交互:
1 RN侧主动调用原生模块方法:回调函数和Promise机制
首先在电脑中部署Android开发和RN开发所需要的环境:
打开Android端的初始化项目(名为MyRNProject):
工程中只有一个Activity,且继承于ReactActivity,将该Activity设置为应用的入口,getMainComponentName()返回服务器上对应的组件名称
package com.myrnproject;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "MyRNProject";
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myrnproject"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
MainApplication为工程的Application,同样需要继承ReactApplication,getJSMainModuleName()函数返回加载的第一个js文件名称。将需要注册的ReactPackage添加到这里protected List getPackages(){},new MyRNPackage()为我们自己定义的ReactPackage,该类用来创建工程中的NativeModule以及ViewManager等类。NativeModule为原生为RN提供的方法,ViewManager为供RN使用的原生组件。
package com.myrnproject;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MyRNPackage()
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
package com.myrnproject;
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.List;
/**
* Created by tianxiying on 2017/12/6.
*/
public class MyRNPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> moudles = new ArrayList<>();
moudles.add(new AddressModule(reactContext)); //加入开发接口
moudles.add(new ToastModule(reactContext));
moudles.add(new EventModule(reactContext));
return moudles;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> list = new ArrayList<ViewManager>();
list.add(new MyTextViewManager());
list.add(new MyCalendarViewManager());
return list;
}
}
接下来我要为RN提供一个安卓原生代码的方法,用Toast举例:我创建了一个ToastModule继承于ReactContextBaseJavaModule,getName() 函数返回模块名称,在RN中通过该名称找到该模块,在上面的MyRNPackage中我们已将其加入管理器中。
内部代码如下:
package com.myrnproject;
import android.os.Handler;
import android.widget.Toast;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
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 tianxiying on 2017/12/14.
*/
public class ToastModule extends ReactContextBaseJavaModule {
private ReactApplicationContext aContext;
private Promise interfacePromise;
private Handler handler;
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext reactContext) {
super(reactContext);
aContext = reactContext;
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
@Override
public String getName() {
return "ToastModule";
}
@ReactMethod
public void showToast(String message, int duration, Callback callback) {
try {
Toast.makeText(aContext, message, duration).show();
callback.invoke("success");
} catch (Exception e) {
callback.invoke("exception" + e.toString());
}
}
@ReactMethod
public void showToastLater(String message, int duration, final Callback callback) {
try {
Toast.makeText(aContext, message, duration).show();
handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
doCallBack(callback);
}
}, 5000);
} catch (Exception e) {
callback.invoke("exception" + e.toString());
}
}
public void doCallBack(Callback callback) {
callback.invoke("原生5秒后回调CallBack");
}
}
1.1 回调函数:
RN调用原生方法,然后在原生侧调用RN侧提供的回调函数,实现交互。
以ToastModule为例,加上@ReactMethod注解的方法为RN模块可以直接调用方法:
@ReactMethod
public void showToast(String message, int duration, Callback callback) {
try {
Toast.makeText(aContext, message, duration).show();
callback.invoke("success");
} catch (Exception e) {
callback.invoke("exception" + e.toString());
}
}
该方法有三个参数,分别是信息字符串、显示时长以及RN端传入的回调,getConstants()中约定了对应的变量方便找RN端使用。
在RN端的代码如下:首先将ToastModule导入,在showToast函数中直接调用ToastModule.showToast(),将需要参数包含回调函数传入。回调成功将改变state中的变量值。
import React, {Component} from 'react';
import {
View,
Text,
Image,
StyleSheet,
PixelRatio,
TouchableOpacity,
NativeModules,
DeviceEventEmitter,
Platform,
} from 'react-native';
let ToastModule = NativeModules.ToastModule;
...
<Text style={styles.text_title}>Toast(回调函数)</Text>
<TouchableOpacity onPress={(message) => this.showToast("hello world!")}>
<Text style={styles.text_item}>
{"回调结果:" + this.state.interfaceResult}
</Text>
</TouchableOpacity>
...
showToast(message) {
console.log("showToast!");
if (Platform.OS === "android") {
ToastModule.showToast(message,
ToastModule.SHORT,
(msg) => {
this.setState({
interfaceResult: msg
})
});
}
}
原生代码调用了回调函数,回调函数并不会马上执行,因为是混合开发的桥接方式本来就是异步的)
注意事项:需要注意的是,回调函数必须要RN侧提供的情况下才能调用,调用Callback.invoke()即可执行回调方法,也可以将Callback引用暂时保存起来,不立即调用,但是该回调方法只能调用一次,多于一次将会如下错误:
1.2 Promise机制
建议使用,因为Promise在RN中使用非常广泛,能力也非常强大。
下面我在RN模块中,调用原生代码读取手机通讯录并将其显示在界面上:
package com.myrnproject;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
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;
import com.facebook.react.bridge.ReactMethod;
import org.json.JSONObject;
import static android.app.Activity.RESULT_OK;
/**
* Created by tianxiying on 2017/12/6.
*/
public class AddressModule extends ReactContextBaseJavaModule {
private ReactApplicationContext aContext;
private Promise interfacePromise;
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode != 1 || resultCode != RESULT_OK) return;
Uri contactData = data.getData();
Cursor cursor = activity.managedQuery(contactData, null, null, null, null);
cursor.moveToFirst();
String toRNMessage = getContactInfo(cursor);
if (toRNMessage != null) {
interfacePromise.resolve(toRNMessage);
}
}
};
private String getContactInfo(Cursor cursor) {
try {
String name = "";
String phoneNumber = "";
int idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID);
String contactId = cursor.getString(idColumn);
String queryString = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" + contactId;
Uri aUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
Cursor phone = aContext.getContentResolver().query(aUri, null, queryString,
null, null);
String dn = ContactsContract.Contacts.DISPLAY_NAME;
String pn = ContactsContract.CommonDataKinds.Phone.NUMBER;
if (phone.moveToFirst()) {
for (; !phone.isAfterLast(); phone.moveToNext()) {
dn = name = cursor.getString(cursor.getColumnIndex(dn));
phoneNumber = phone.getString(phone.getColumnIndex(pn));
}
phone.close();
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("msgType","pickContactResult");
jsonObject.put("displayName",name);
jsonObject.put("peerNumber",phoneNumber);
return jsonObject.toString();
} catch (Exception e) {
interfacePromise.reject("error while get contact", e);
}
return null;
}
public AddressModule(ReactApplicationContext reactContext) {
super(reactContext);
aContext = reactContext;
reactContext.addActivityEventListener(mActivityEventListener);
}
@Override
public String getName() {
return "AddressModule";
}
@ReactMethod
public void handleMessage(String aMessage, Promise aPromise) {
interfacePromise = aPromise;
Intent aIntent = new Intent(Intent.ACTION_PICK);
aIntent.setType(ContactsContract.Contacts.CONTENT_TYPE);
Bundle b = new Bundle();
aContext.startActivityForResult(aIntent, 1, b);
}
}
重点关注一下 public void handleMessage(String aMessage, Promise aPromise)函数,它有两个参数,分别是一个消息字符串和一个Promise引用,在该方法被调用时我们将Promise引用保存下来为interfacePromise,ActivityEventListener负责监听Activity的生命周期,我们已将其注册,在通讯录页面返回的时候,onActivityResult将被调用,我们将用户点击的通讯录好友信息获取到保存为Json格式,使用interfacePromise.resolve(toRNMessage)将其返回到RN端。
RN端代码如下:在then函数中处理成功回调,在catch处理异常回调。
import React, {Component} from 'react';
import {
View,
Text,
Image,
StyleSheet,
PixelRatio,
TouchableOpacity,
NativeModules,
DeviceEventEmitter,
Platform,
} from 'react-native';
let AddressModule = NativeModules.AddressModule;
...
<Text style={styles.text_title}>读取通讯录(Promise机制)</Text>
<TouchableOpacity onPress={() => this.userPressAddressBook()}>
<Text style={styles.text_item}>
{this.state.ContactName + ":" + this.state.PhoneNumber}
</Text>
</TouchableOpacity>
...
userPressAddressBook() {
console.log("clickUserPressAddressBook!");
AddressModule.handleMessage("testMessage").then(
(result) => {
console.log(result);
let aObj = JSON.parse(result);
this.setState({
PhoneNumber: aObj.peerNumber,
ContactName: aObj.displayName,
})
}
).catch(
(error) => {
console.log(error);
}
);
}
Promise对比回调机制不需要设置和实现事件的监听函数,通过Promise机制让异步处理便于书写、阅读与理解。
2 原生代码主动调用RN代码:通过SendEvent——发送消息的方式。
首先,我定义了一个EventModule用来主动发送消息:
package com.myrnproject;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import javax.annotation.Nullable;
/**
* Created by tianxiying on 2017/12/14.
*/
public class EventModule extends ReactContextBaseJavaModule {
private ReactApplicationContext aContext;
public EventModule(ReactApplicationContext reactContext) {
super(reactContext);
aContext = reactContext;
}
@Override
public String getName() {
return "EventModule";
}
public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
@ReactMethod
public void testSendEvent() {
WritableMap parms = Arguments.createMap();
sendEvent(aContext,"testEvent",parms);
}
}
重点关注sendEvent方法,通过reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params),其中eventName代表消息名,params代表参数。
为了方便测试,我写了一个testSendEvent方法,由RN界面来触发sendEvent方法。
RN中代码如下:在componentDidMount进行了监听事件的注册。
import React, {Component} from 'react';
import {
View,
Text,
Image,
StyleSheet,
PixelRatio,
TouchableOpacity,
NativeModules,
DeviceEventEmitter,
Platform,
} from 'react-native';
let EventModule = NativeModules.EventModule;
...
componentDidMount() {
DeviceEventEmitter.addListener('testEvent', (message) => {
// handle event.
console.log("testEvent!" + message);
this.setReceivedResult();
});
}
...
<Text style={styles.text_title}>原生调RN(sendEvent)</Text>
<TouchableOpacity onPress={() => this.testSendEvent()}>
<Text style={styles.text_item}>
{"消息结果:" + this.state.sendEventResult}
</Text>
</TouchableOpacity>
...
testSendEvent() {
console.log("testSendEvent!");
EventModule.testSendEvent();
}
...
setReceivedResult() {
console.log("set received!");
this.setState({
sendEventResult: 'received sendevent'
});
}