ReactNative-调用原生开启新页面-Android篇

开启新页面-Andorid篇

最近在研究ReactNative,想用于新的项目开发,发现我们传统的Android中开Activity的方式没有了,只能通过导航控制器来实现,但是导航控制器本身又很难实现设计MM出的效果,故考虑自己写一个源生模块来实现交互,然后导航控制器自定义就好了

前面

何谓开启新页面呢?andorid中有两种描述页面的方式,一个是Activity,一种是Fragment
我这里的开启新页面就是使用Intent的方式开启Activity
这里就是使用ReactNative(后称RN)开启新页面

项目

项目地址
目前项目托管在oschina上,后续迁移到github

开发环境

macos,用windows/linux的请自行探索相关的开发步骤或者环境

node版本
npm版本
RN的版本
package.json 详见截图


屏幕快照 2017-07-14 上午8.31.48.png

使用WebStrom开发js部分
AndroidStudio开发Android的原生部分

思路分析

分析官网模块的注入

首先肯定要先可以完成交互,再考虑如何去实现

官方文档

android原生模块
这里详细解说了如何使用js调用原生模块

我们都知道ReactNative本身还是渲染js脚本来形成源生控件,而android必须要有一个Activity来作为载体

  1. 创建原生模块
  2. 将原生模块注入application
  3. 调用源生代码

动手写代码

首先创建一个模块

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的人来说还算可以看看的文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容