背景(实现的原因):
有时候我们的项目需要:
- 访问Android平台上的API时, 但是我们的React Native可能还没有相应的模块包装;
- 再或者我们需要复用一些Java代码的时候, 而不是用JavaScript重新实现时;
- 又或者我们需要实现一些高性能的, 多线程的(因为JavaScript只跑在一个线程上)的代码, 譬如: 操作数据库, 加载图片等等的时候;
这时候我们可以采用封装的方式, 将这些高级特性(平台上的API)封装成一个模块, 这样可以提高我们的工作效率, 减少许多代码量;
接下来我们来实现Android上的Toast模块, 为什么实现这个模块? 因为这个模块较为简单, 而且我们开发项目时也经常用到; 假设我们的希望从JavaScript发起一个Toast消息(React Native中内置了一个名为ToastAndroid的模块);
实现步骤:
1. 首先我们创建一个原生模块, 将这个模块命名为:ToastModule, 一个原生模块是集成了 ReactContextBaseJavaModule 的Java类, 它可以实现一些JavaScript所需的功能; 在这里我们的目标是在JavaScript的代码里, 当我们点击一个Button的时候, 会弹出一个类似Android里的Toast通知:
代码如下:
public class ToastModule extends ReactContextBaseJavaModule {
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Nullable
@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;
}
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
@Override
public String getName() {
return "ToastExample";
}
}
> 注意: ReactContextBaseJavaModule 要求派生类实现 getName() 方法, 这个方法用来返回一个字符串(这个字符串很重要, 在后面的JavaScript代码里自定义原生模块的时候, 使用的就是该字符串, 在JavaScript端用来标记这个模块 ) ;
@Override
public String getName() {
//如果模块有RCT前缀,那么这个前缀会被自动清除, 所以返回的字符串如果为RCTToastExample,
//那么在JavaScript端依然通过React.NativeModules.ToastExample访问到这个模块;
return "ToastExample";
}
接下来, 有一个可选的方法getContants()方法, 用来返回需要导出给JavaScript使用的常量(这个方法不一定需要实现, 但是定义一些可以被JavaScript同步访问到的预定义的值时还是非常有用的)
@Nullable
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("SHORT", Toast.LENGTH_SHORT);
constants.put("LONG", Toast.LENGTH_LONG);
return constants;
}
在我们自定义的原生模块中, 最后一步就是要导出一个方法给JavaScript使用, 这时候Java方法需要使用到@ReactMethod这个注解, 这个方法的返回值必须为null; 因为React Native的跨语言访问是异步进行的, 所以想要给JavaScript返回一个值的唯一办法是使用回调函数或者发送事件;
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
2.注册模块
> 我们想要使用这个模块, 那么我们首先需要做的事情就是注册这个模块, 这时候我们需要在应用的Package类的 createNativeModules 方法中添加这个模块, 如果我们没注册这个模块, 那么我们无法在JavaScript中访问到这个模块:
public class AnExampleReactPackage implements ReactPackage {
//这个方法为注册我们自定义的模块;
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ToastModule(reactContext));
return modules;
}
//这个方法为注册我们的自定义的组件(以后的原生组件会讲到);
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
然后这个package需要在MainApplication.java文件中的getPackages()方法中提供, 这个时候我的demo中, 我自己创建了一个新的MyApplication, 继承了Application, 并且实现了ReactApplication, 只不过我们创建完这个Application后 ,我们需要在Android Manifest中的application节点中设置name: MyApplication, 不了解的可以先去看我第一篇文章《初识React Native(一)—集成到原生Android项目》 :
public class MyApplication extends Application implements ReactApplication {
private final ReactNativeHost reactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(new MainReactPackage(),
new AnExampleReactPackage());
}
};
...
}
3.运行我们的原生模块:
> 我们通常可以把原生模块封装成一个JavaScript模块, 这样我们从JavaScript端访问起来更加方便, 但是这不是必须的, 但是我依然封装成了JavaScript模块, 我的封装原生模块的ToastModule.js文件:
import { NativeModules } from 'react-native';
export default NativeModules.ToastExample;
最后就是在我们React Native界面上使用这个模块了, 在这我生成一个Button, 当我们点击这个Button的时候, 就会弹出这个Toast通知:
...
import ToastExample from './ToastModule';
...
...
export default class HelloReactNative extends React.Component {
_onPressButton() {
console.log("you tapped the button !");
ToastExample.show('clicked me', ToastExample.SHORT);
}
render() {
return (
<View style={styles.container}>
...
<TouchableNativeFeedback onPress={this._onPressButton}>
<Text style={styles.button}>Button</Text>
</TouchableNativeFeedback>
...
</View>
)
}
}
效果图: