编写 Android 原生模块的若干提示
假设你已经阅读了官方文档 Android Native Modulesopen in new window,并且已经学会如何编写一个简单的 Android 原生模块。
使用脚手架创建原生模块
可以使用 react-native-create-libopen in new window 来创建原生模块
例如:
npx react-native-create-lib \
--module-name @sdcx/wechat \
--repo-name react-native-wechat \
wechat
原生模块必须具有类型定义
没有类型的代码是难以维护的。我们要求为导出的原生模块声明完整的类型。
这些类型定义同时作为接口和协议存在,方便多端同事协作,不管是编写原生模块,还是使用原生模块。
import { NativeModules, NativeModule } from "react-native"
interface PayInfo {
partnerId: string
prepayId: string
nonceStr: string
timeStamp: number
sign: string
package: string
}
interface WechatModule extends NativeModule {
registerApp(appId: string, universalLink: string): Promise<boolean>
isWXAppInstalled(): Promise<boolean>
sendAuthRequest(): Promise<string>
pay(info: PayInfo): Promise<string>
}
const WechatModule: WechatModule = NativeModules.WechatModule
export default WechatModule
不要直接导出原生模块给到用户
我们不能把原生模块直接暴露给用户,而是先要包裹一下。
如果模块比较简单,可以将原生模块中的每个方法分别包裹为函数,然后以命名空间的形式导出。
// wechat.ts
import { NativeModules } from "react-native"
const WechatModule: WechatModule = NativeModules.WechatModule
export function registerApp(appId: string, universalLink: string): Promise<boolean> {
return WechatModule.registerApp(appId, universalLink)
}
export function isWXAppInstalled(): Promise<boolean> {
return WechatModule.isWXAppInstalled()
}
export function sendAuthRequest(): Promise<string> {
return WechatModule.sendAuthRequest()
}
// index.ts
export * as WechatSDK from "./wechat"
如果模块比较复杂,需要在 React 侧做一些逻辑处理,那么可以将原生模块包裹到一个对象中导出。
// index.ts
import { NativeModules } from "react-native"
const WechatModule: WechatModule = NativeModules.WechatModule
const WechatSDK = {
registerApp: function (appId: string, universalLink: string) {
return WechatModule.registerApp(appId, universalLink)
},
isWXAppInstalled: function () {
return WechatModule.isWXAppInstalled()
},
sendAuthRequest: function () {
return WechatModule.sendAuthRequest()
},
}
export default WechatSDK
转换 ReadableMap 参数为模型
在处理 React 侧传递过来的参数时,将 ReadableMap 转换为原生模型,是个较好的模式。
@ReactMethod
public void launchCamera(final ReadableMap options, final Callback callback) {
this.options = new Options(options);
}
public class Options {
Boolean pickVideo = false;
Boolean includeBase64;
int videoQuality = 1;
int quality;
Options(ReadableMap options) {
if (options.getString("mediaType").equals("video")) {
pickVideo = true;
}
includeBase64 = options.getBoolean("includeBase64");
String videoQualityString = options.getString("videoQuality");
if(!TextUtils.isEmpty(videoQualityString) && !videoQualityString.toLowerCase().equals("high")) {
videoQuality = 0;
}
quality = (int) (options.getDouble("quality") * 100);
}
}
Arguments.toBundle()
谨慎使用 某些时候,可能需要使用 Arguments.toBundle()
将 React Native 传递过来的数据转换成 Bundle。
不过此时务必小心:所有的 number 都被转换成了 double
@ReactMethod
public void pay(@Nonnull ReadableMap data, Promise promise) {
Bundle bundle = Arguments.toBundle(data);
int count = (int)bundle.getDouble("count");
}
总是在主线程操作 UI
可以使用 UiThreadUtil
检测当前是否主线程,调度任务到主线程执行
@ReactMethod
public void isWXAppInstalled(Promise promise) {
UiThreadUtil.runOnUiThread(() -> wechatHelper.isWXAppInstalled(promise));
}
JavaModule 是个门面
这里的 JavaModule 是指继承了 ReactContextBaseJavaModule
的类
说这种类是个门面,有两层意思。
其一,JavaModule 是连接 Native 和 React 两个世界的桥梁,只起连接作用,真正干活的另有其人。
譬如下面这个微信 SDK 例子,真正的活被转发给了 wechatHelper
,原生模块负责线程切换。
// WechatModule.java
@ReactMethod
public void pay(@Nonnull ReadableMap data, Promise promise) {
UiThreadUtil.runOnUiThread(() -> wechatHelper.pay(data, promise));
}
其二,如果原生代码需要用到的功能和原生模块相关,那么由 JavaModule 负责转发。
还是微信 SDK 的例子,我们可以在 WechatModule
内部暴露一个静态方法供原生代码使用,而不直接暴露 wechatHelper
,否则,我们可能需要将 wechatHelper
做成单例。
public void handleIntent(Intent intent) {
wechatHelper.handleIntent(intent);
}
public static void handleIntent(@Nullable ReactContext reactContext, Intent intent) {
if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
WechatModule wechatModule = reactContext.getNativeModule(WechatModule.class);
if (wechatModule != null) {
wechatModule.handleIntent(intent);
}
}
}
然后我们在 WXEntryActivity
或 WXPayEntryActivity
中可以通过如下方式调用 handleIntent
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
WechatModule.handleIntent(MainApplication.get().getCurrentReactContext(), getIntent());
finish();
}
与 React 侧通信前,先检测 ReactContext 是否存活
譬如向 React 侧发送事件时
public void sendEvent(@NonNull String eventName, @NonNull WritableMap params) {
if (reactContext.hasActiveCatalystInstance())) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
当 ReactContext 被销毁时,释放相关资源
如果模块持有资源,那么在 onCatalystInstanceDestroy
中释放它们。
// WechatModule.java
@Override
public void onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy();
this.wechatHelper.handleReload();
}
// WechatHelper.java
public void handleReload() {
if (broadcastReceiver != null) {
context.unregisterReceiver(broadcastReceiver);
}
}
发生错误时,总是返回 code 和 message
记住,code 是个字符串。
我们遵循这么一个原则:code 用来进一步分析问题,message 则可以直接提示给用户。
promise.reject(errorCode(resp.errCode), resp.errStr);
private String errorCode(int errCode) {
Map<Integer, String> map = new HashMap<>();
map.put(BaseResp.ErrCode.ERR_OK, "WXSuccess");
map.put(BaseResp.ErrCode.ERR_COMM, "WXErrCodeCommon");
map.put(BaseResp.ErrCode.ERR_USER_CANCEL, "WXErrCodeUserCancel");
map.put(BaseResp.ErrCode.ERR_SENT_FAILED, "WXErrCodeSentFail");
map.put(BaseResp.ErrCode.ERR_AUTH_DENIED, "WXErrCodeAuthDeny");
map.put(BaseResp.ErrCode.ERR_UNSUPPORT, "WXErrCodeUnsupport");
map.put(BaseResp.ErrCode.ERR_BAN, "WXErrCodeBan");
return map.get(errCode);
}
在 React 侧,可以通过以下方式获取 code 和 message
try {
// ...
} catch (error) {
console.log(error.code, error.message)
}
正确使用事件
和回调或 Promise
只能生效一次不同,事件可以源源不断地从原生侧传递给 React 侧。
在 Native 侧
通过 getConstants
导出事件名为常量
public static final String WECHAT_STAFF_EVENT = "WECHAT_STAFF_EVENT";
@Nullable
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("WECHAT_STAFF_EVENT", WECHAT_STAFF_EVENT);
// ...
return constants;
}
像如下那样发布事件
public void sendEvent(@NonNull String eventName, @NonNull WritableMap params) {
if (reactContext.hasActiveCatalystInstance())) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
在 React 侧,使用 NativeEventEmitter
包裹原生模块,得到一个 EventEmitter
实例
import { NativeModules, NativeModule, NativeEventEmitter } from "react-native"
interface WechatModule extends NativeModule {
WECHAT_STAFF_EVENT: string
}
const WechatModule: WechatModule = NativeModules.WechatModule
const WechatEventEmitter: NativeEventEmitter = new NativeEventEmitter(WechatModule)
export { WechatEventEmitter, WechatModule }
像如下那样监听事件
useEffect(() => {
const subscription = WechatEventEmitter.addListener(WechatModule.WECHAT_STAFF_EVENT, () => {
// do something
})
return () => {
subscription.remove()
}
})
正确处理需要系统权限的操作
某些涉及隐私的操作,需要用户授予权限才能进行。譬如获取用户位置,访问相册等等。
我们遵循职责分离的原则,原生模块不负责请求权限,仅在执行操作前,检查是否拥有执行操作所需权限,如果没有,则通过 Error 机制把异常信息返回给 React 侧,由 React 侧进行处理。
@ReactMethod
public void launchCamera(final ReadableMap options, final Promise promise) {
if (!isCameraPermissionFulfilled(reactContext, currentActivity)) {
promise.reject('CameraPermisseionError', '缺少相机权限')
return;
}
}
在 React 侧,我们使用 react-native-permissionsopen in new window 处理权限的检查和请求。
通常会使用高阶函数,把需要权限的请求包裹一下。
// PermissionUtil.ts
export function withCameraPermission<T>(fn: () => Promise<T>) {
if (Platform.OS === "ios") {
return withCameraPermissionIOS(fn)
} else {
return withCameraPermissionAndroid(fn)
}
}
// App.tsx
const handleAvatarPress = async () => {
const result = await withCameraPermission(pickImageFromCamera)()
}
将服务鉴权委托给主工程
有时,我们封装的原生组件库,可能需要访问我们自己的服务,而这些服务需要鉴权。
我们的组件库是集成到我们的主工程中使用的,而我们的主工程已经拥有一套鉴权机制来获取 token,刷新 token。
我们的组件库应该复用主工程这套鉴权机制。
可我们的组件库又不能依赖我们的主工程,该怎么办呢?
答案是控制反转。我们在组件库声明一套鉴权接口,由主工程注入实现。
// 组件库
export interface TokenProvider {
refreshToken: () => Promise<string>
getAccessToken: () => Promise<string>
}
const Track = {
setTokenProvider: function (tokenProvider: TokenProvider) {
_tokenProvider = tokenProvider
},
}
// 主工程
Track.setTokenProvider({
refreshToken: AuthService.refreshToken,
getAccessToken: AuthService.getAccessToken,
})
不仅仅是服务鉴权,任何组件库不知道怎么办,或不是它的职责的事情,都可以通过控制反转委托给主工程来处理。
避免 onActivityResult 陷阱
有时,我们需要启动其它 Activity 并通过 onActivityResult
来接收其返回来的结果。
此时要注意 requestCode
不要和其它组件库有所冲突,并且如果 requestCode
不属于本组件库,要尽早返回。
如下所示
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (!isValidRequestCode(requestCode)) {
return;
}
}
合理使用请求-响应模式
某些请求是异步的,并需要支持并发,此时,可以使用请求-响应模式。
譬如下面这个例子,我们需要通过地理坐标来获取地区编码
// LocationModule.java
@ReactMethod
public void adcode(double lat, double lon, final Promise promise) {
AdcodeRequest adcodeRequest = new AdcodeRequest(getReactApplicationContext());
adcodeRequest.execute(lat, lon, new AdcodeRequest.AdcodeRequestListener() {
@Override
public void onSuccess(@Nonnull String adcode) {
promise.resolve(adcode);
}
@Override
public void onFailure(@Nonnull String errorInfo) {
promise.reject(ErrorCode, errorInfo);
}
});
}
// AdcodeRequest.java
public class AdcodeRequest {
public interface AdcodeRequestListener {
void onSuccess(@Nonnull String adcode);
void onFailure(@Nonnull String errorInfo);
}
private final Context mContext;
public AdcodeRequest(Context context) {
mContext = context;
}
public void execute(double lat, double lon, @Nonnull AdcodeRequestListener listener) {
RegeocodeQuery query = new RegeocodeQuery(new LatLonPoint(lat, lon), 200f, GeocodeSearch.AMAP);
GeocodeSearch geocodeSearch = new GeocodeSearch(mContext.getApplicationContext());
geocodeSearch.setOnGeocodeSearchListener(new GeocodeSearch.OnGeocodeSearchListener() {
@Override
public void onRegeocodeSearched(RegeocodeResult result, int code) {
if (code == 1000) {
String adCode = result.getRegeocodeAddress().getAdCode();
listener.onSuccess(adCode);
} else {
listener.onFailure("未能获得地理编码:" + code);
}
}
@Override
public void onGeocodeSearched(GeocodeResult geocodeResult, int code) {
}
});
geocodeSearch.getFromLocationAsyn(query);
}
}
别忘了混淆规则
如果引用了第三方库,别忘了在组件库的 proguard-rules.pro 文件中设置混淆规则。