開啟新頁面-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的人來說還算可以看看的文章