开启新页面-Andorid篇
最近在研究ReactNative,想用于新的项目开发,发现我们传统的Android中开Activity的方式没有了,只能通过导航控制器来实现,但是导航控制器本身又很难实现设计MM出的效果,故考虑自己写一个源生模块来实现交互,然后导航控制器自定义就好了
前面
何谓开启新页面呢?andorid中有两种描述页面的方式,一个是Activity,一种是Fragment
我这里的开启新页面就是使用Intent的方式开启Activity
这里就是使用ReactNative(后称RN)开启新页面
项目
项目地址
目前项目托管在oschina上,后续迁移到github
开发环境
macos,用windows/linux的请自行探索相关的开发步骤或者环境
node版本
npm版本
RN的版本
package.json 详见截图
使用WebStrom
开发js
部分
AndroidStudio
开发Android
的原生部分
思路分析
分析官网模块的注入
首先肯定要先可以完成交互,再考虑如何去实现
官方文档
android原生模块
这里详细解说了如何使用js调用原生模块
我们都知道ReactNative本身还是渲染js脚本来形成源生控件,而android必须要有一个Activity来作为载体
- 创建原生模块
- 将原生模块注入application
- 调用源生代码
动手写代码
首先创建一个模块
package com.sxwphone;
import android.app.Activity;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
/**
* Created by cai on 2017/7/13.
*/
public class StartNewHelper extends ReactContextBaseJavaModule {
public StartNewHelper(ReactApplicationContext reactContext) {
super(reactContext);
}
@ReactMethod
public void startNewActivity(String name) {
Activity activity = getCurrentActivity();
if (activity instanceof StartNewActivity) {
((StartNewActivity) activity).startNewActivity(name);
}
}
@Override
public String getName() {
return "startNew";
}
}
首先是一个模块,这个模块的名字是startNew
也就是getName()
中的返回值后面我们会用到它
这里吐槽自己一下,这类名起的真烂,让后续维护的人没法用啊(实际项目中不要这样随意,否则会被骂死的)
这里的@ReactMethod
标识的方法startNewActivity(String name)
就是后续js要用到的方法,这里记录一下
关联模块
我们有了自己的模块,得将它与项目关联起来
首先需要创建一个ReactPackage,将模块注入其中
package com.sxwphone;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
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;
public class ExampleReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new StartNewHelper(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
这里在List<NativeModule> createNativeModules(ReactApplicationContext reactContext)
中将module加到集合里
这里还需要将package
加入到ReactNativeHost
中
package com.sxwphone;
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.asList(new MainReactPackage(), new ExampleReactPackage());
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
这个是application的代码,其中有一个ReactNativieHost
,将我们的ExampleReactPackage
加入到List<ReactPackage> getPackages()
创建的集合中,这样我们就完成了Native模块的注入
js调用
到了这里我们就可以通过js调用到方法了
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import {
AppRegistry, Button,
StyleSheet,
Text,
View
} from 'react-native';
import {NativeModules} from 'react-native';
export default class sxwphone extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.android.js
</Text>
<Text style={styles.instructions}>
Double tap R on your keyboard to reload,{'\n'}
Shake or press menu button for dev menu
</Text>
<Button title={'点击'} onPress={() => this.newPage()}/>
</View>
);
}
newPage() {
var startHelper = NativeModules.startNew;
startHelper.startNewActivity("abc")
}
// newPage() {
//
// }
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('sxwphone', () => sxwphone);
AppRegistry.registerComponent('abc', () => sxwphone);
这里我是比较懒,没有写多个模块,等于是同一个页面,只是开在不同的两个Activity上了
这个时候运行应用
Activity
package com.sxwphone;
/**
* Created by cai on 2017/7/13.
*/
public interface StartNewActivity {
void startNewActivity(String name);
String getMainComponentName();
}
MainActivity:
@Override
public void startNewActivity(String name) {
Intent intent = new Intent(this, NewActivity.class);
intent.putExtra(NewActivity.NAME, name);
startActivity(intent);
}
NewActivity:
@Nullable
@Override
public String getMainComponentName() {
if (getIntent() == null) {
return null;
}
String name = getIntent().getStringExtra(NAME);
if (name == null || name.isEmpty()) {
finish();
return "";
}
return name;
}
这里我重写了getMainComponentName()
方法,不直接返回字符串了,返回一个从上个页面传来的值也就是abc
这样应该可以调用到对应的模块了吧
接下来运行吧
运行
运行andorid,发现崩溃了,崩溃了....
查下原因:
NoFountActivity
哦哦 没注册Activity啊 打开AndoridManifest.xml注册下
这个时候以为结束了?太天真了!!!
发现这时候新页面打开了,咋是空白一片呢?
这个时候就要考虑问题出在哪里了呢
解决方案
我们MainActivity直接返回模块名就成功了,为啥这里不成功呢,Intent的传递一定没错,那么错在哪里呢
这个时候应该想如何去解决这样的问题了,为啥会空白一片呢
我们考虑是不是调用时机出现了问题呢?
查看Android代码
打开MainActivity
,发现MainActivity
是继承自ReactActivity
的
这时候打开ReactActivity
发现是直接继承自Activity的,那么具体实现就在这里了
onCreate
方法中有一个delegate,我们发现所有的Activity生命周期方法都和delegate关联了
具体实现查看下delegate 是ReactActivityDelegate
ReactActivityDelegate
private final @Nullable String mMainComponentName;
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
mFragmentActivity = null;
}
public ReactActivityDelegate(
FragmentActivity fragmentActivity,
@Nullable String mainComponentName) {
mFragmentActivity = fragmentActivity;
mMainComponentName = mainComponentName;
mActivity = null;
}
protected void onCreate(Bundle savedInstanceState) {
boolean needsOverlayPermission = false;
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(getContext())) {
needsOverlayPermission = true;
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
}
}
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
这里会发现我们在MainActivity中实现的mMainComponentName就是被用到这里了
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
}
而且这个是一个final字段,我们不能改写,而这个name又是在Activity的构造方法中传入的
作为多年的Android程序员,我们知道Activity这个东西是由ActivityThread创建的,这个时候Intent还没生效呢呢,而构造方法又必然首先运行,所以运行顺序是
Activity 构造方法->Activity.getMainComponentName()->null
这里如果就这样运行的话,无论如何也只能获得空,我们必须让loadApp可以运行,且componentName不是空
这里牵扯到两种写法,我在onCreate前调用Intent,获取到Component的名字,通过暴力反射的方式,修改名称,这里可以这样写,但是我想了一下,放弃了,反射影响效率,而且代码不优雅,所以考虑使用别的方案解决
这里查看下loadApp的调用,发现onActivityResult()中也有执行
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult(getPlainActivity(), requestCode, resultCode, data);
} else {
// Did we request overlay permissions?
if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(getContext())) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
}
}
}
}
这里是为什么呢?
这就牵扯到Android6.0的运行时权限了
这里如果检查到运行时权限没通过,就需要到activityResult中再执行加载界面的代码
MyReactActivityDelegate
既然我们无法复写final的方法,那就需要我们创建自己的Delegate
继承ReactActivityDelegate
public class MyReactActivityDelegate extends ReactActivityDelegate {
private final Activity activity;
private final String firstMainComponentName;
private static final String REDBOX_PERMISSION_GRANTED_MESSAGE =
"Overlay permissions have been granted.";
public MyReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
super(activity, mainComponentName);
this.activity = activity;
firstMainComponentName = mainComponentName;
}
public MyReactActivityDelegate(FragmentActivity fragmentActivity, @Nullable String mainComponentName) {
super(fragmentActivity, mainComponentName);
this.activity = fragmentActivity;
firstMainComponentName = mainComponentName;
}
private boolean isLoadApp = false;
public boolean isLoadApp() {
return isLoadApp;
}
@Override
protected void loadApp(String appKey) {
if (activity instanceof StartNewActivity) {
if (isLoadApp()) {
return;
}
String mainComponentName = ((StartNewActivity) activity).getMainComponentName();
super.loadApp(mainComponentName);
isLoadApp = true;
} else {
super.loadApp(appKey);
}
}
private static final String REDBOX_PERMISSION_MESSAGE =
"Overlay permissions needs to be granted in order for react native apps to run in dev mode";
private static final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
@Override
protected void onCreate(Bundle savedInstanceState) {
String mMainComponentName = null;
if (activity instanceof StartNewActivity) {
mMainComponentName = ((StartNewActivity) activity).getMainComponentName();
}
boolean needsOverlayPermission = false;
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(activity)) {
needsOverlayPermission = true;
}
}
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
return;
}
super.onCreate(savedInstanceState);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
String mMainComponentName = null;
if (activity instanceof StartNewActivity) {
mMainComponentName = ((StartNewActivity) activity).getMainComponentName();
}
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult((Activity) getContext(), requestCode, resultCode, data);
} else {
// Did we request overlay permissions?
if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(getContext())) {
if (firstMainComponentName != null) {
loadApp(firstMainComponentName);
} else if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
}
}
}
}
protected Context getContext() {
return activity;
}
}
这里将activity和原始的componentName都作为成员变量写了下来,方便后面的调用
在onCreate的时候检查权限和当时的方法中获取的名字,如果不是空,则loadApp
activityResult中同理,如果最初的name不为空,则加载最初的名字,如果为空,则继续判断方法中获取的名字
接着修改NewActivity
package com.sxwphone;
import android.content.Intent;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import javax.annotation.Nullable;
/**
* Created by cai on 2017/7/13.
*/
public class NewActivity extends ReactActivity implements StartNewActivity {
public static final String NAME = "_name";
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new MyReactActivityDelegate(this, getMainComponentName());
}
@Nullable
@Override
public String getMainComponentName() {
if (getIntent() == null) {
return null;
}
String name = getIntent().getStringExtra(NAME);
if (name == null || name.isEmpty()) {
finish();
return "";
}
return name;
}
@Override
public void startNewActivity(String name) {
Intent intent = new Intent(this, NewActivity.class);
intent.putExtra(NewActivity.NAME, name);
startActivity(intent);
}
}
运行
发现点击按钮就可以调用到新模块了
修改完的最终代码查看 项目地址
总结
这个项目没用多长时间就完成了,但是可以说初窥了RN和android的交互,RN的模块如何注入,其中牵扯到了一些android的知识,RN的一部分知识,可能对于RN+android老手来说没什么,但是我想对于RN新手或者前端转android的人来说还算可以看看的文章