美文网首页ReactNative
ReactNative-调用原生开启新页面-Android篇

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

作者: 做人要简单 | 来源:发表于2017-07-14 09:48 被阅读69次

    开启新页面-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的人来说还算可以看看的文章

    相关文章

      网友评论

        本文标题:ReactNative-调用原生开启新页面-Android篇

        本文链接:https://www.haomeiwen.com/subject/rtknhxtx.html