react-native开发安卓app相关使用总结
记录自己平时使用react-native时遇到的问题。其实大部分问题的解决办法都是看github issues解决的
参考:我之前参考过这篇文章,记录的问题还挺全面,也可以参考这个 react-native版本迁移教程
1. 2018.10.7 升级到0.57.2报错
今天升级了一下reaact-native 版本从0.57.0 -> 0.57.2,metro-react-native-babel-preset:0.48.0
运行packager时候有个报错 ```javascript error: bundling failed: Error: Unable to resolve module `./../../../react-transform-hmr/lib/index.js` from `F:\RN\TS\ph_front_android_medical\src\App.tsx`: The module `./../../../react-transform-hmr/lib/index.js` could not be found from `F:\R N\TS\ph_front_android_medical\src\App.tsx`. Indeed, none of these files exist: * `F:\RN\react-transform-hmr\lib\index.js(.native||.android.js|.native.js|.js|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)` * `F:\RN\react-transform-hmr\lib\index.js\index(.native||.android.js|.native.js|.js|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)` at ModuleResolver.resolveDependency (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\node-haste\DependencyGraph\ModuleResolution.js:209:697) at ResolutionRequest.resolveDependency (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\node-haste\DependencyGraph\ResolutionRequest.js:83:16) at DependencyGraph.resolveDependency (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\node-haste\DependencyGraph.js:222:485) at Object.resolve (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\lib\transformHelpers.js:149:25) at dependencies.map.result (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\DeltaBundler\traverseDependencies.js:316:29) at Array.map (<anonymous>) at resolveDependencies (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\DeltaBundler\traverseDependencies.js:312:16) at F:\RN\TS\ph_front_android_medical\node_modules\metro\src\DeltaBundler\traverseDependencies.js:169:33 at Generator.next (<anonymous>) at step (F:\RN\TS\ph_front_android_medical\node_modules\metro\src\DeltaBundler\traverseDependencies.js:271:307) BUNDLE [android, dev] ./index.js ░░░░░░░░░░░░░░░░ 0.0% (2/174), failed. ``` 解决办法:我用 `react-native start --reset-cache`解决了, 但是貌似其他人用这个并不一定能解决,具体参考这个issues的讨论,遇到的人还挺多
bundling failed: Error: Unable to resolve module /../react-transform-hmr/lib/index.js
2.2018.10.11更新 原生方法里面获取不到当前activity
进入rn时调用原生方法后,原生方法里面获取不到当前activity的问题
安卓通过写nativeModule给rn提供了方法
@ReactMethod public void init(Promise promise) { Activity currentActivity = getCurrentActivity(); Bundle bundle = currentActivity.getIntent().getExtras(); SerializableMap serializableMap = (SerializableMap) bundle.get("map"); WritableMap map = Arguments.createMap(); Iterator<Map.Entry<String, Object>> it = serializableMap.getMap().entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Object> entry = it.next(); map.putString(entry.getKey(), entry.getValue() + ""); if(entry.getValue() instanceof ArrayList){//如果是arraylist需要单独处理 WritableArray writableArray=Arguments.createArray(); for (int i=0;i<((ArrayList) entry.getValue()).size();i++) { writableArray.pushString((String) ((ArrayList) entry.getValue()).get(i)); } map.putArray(entry.getKey(), writableArray); } } promise.resolve(map); }
上面的代码在某些手机中会报 currentActivity为null,因为我调用这个init方法是在进入rn后js立刻调用这个方法的,在某些情况可能activity还没有onHostResume,导致activity为null,所以这里需要做些额外处理
首先在MainActivity中添加一下代码
public static WeakReference<MainActivity> currentActivity;//添加静态弱引用 ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); currentActivity = new WeakReference<>(this); }
然后再上面的init方法里添加一行代码
... if(currentActivity==null&& MainActivity.currentActivity!=null){// 防止进入rn调用init方法时activity还没有onHostResume currentActivity = MainActivity.currentActivity.get(); } ...
现在onCreate里面保存当前activity,然后其他地方就可以用了
3.更新2018.10.27 getCurrentActivity===MainActivity?
此时包为0.57.0版本的react-native
最近了解了一下安卓相关知识,在reactModule里面,我们可以用 getCurrentActivity
来获取MainActivity,但是据我了解,getCurrentActivity怎么就获取的是MainActivity呢?也就是我们RN所在的activity呢?我们从MainActivity可以看到它是继承了 ReactActivity的,而ReactActivity的构造方法里面是调用了createReactActivityDelegate() 这个方法会new 一个ReactActivityDelegate 实例,同时传入this,即当前activity,也就是从这个地方传入的
/** * Called at construction time, override if you have a custom delegate implementation. */ protected ReactActivityDelegate createReactActivityDelegate() { return new ReactActivityDelegate(this, getMainComponentName()); }
疑问1:
那么还有个疑问就是,为什么在这里传入了这个activity后,我们在自己新建的modules里面获取的就是这里传入的呢?我们通过getCurrentActivity()拿到的是
protected @Nullable final Activity getCurrentActivity() { return mReactApplicationContext.getCurrentActivity(); }
也就是我们要找到mReactApplicationContext才能看到activity来自哪里,那再看mReactApplicationContext
public ReactContextBaseJavaModule(ReactApplicationContext reactContext) { mReactApplicationContext = reactContext; }
他是来自我们初始化我们写的package包的时候传入的
public class ReactBridgePackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new ReactBridge(reactContext)); return modules; } }
也就是这里,在构造函数里面传入了reactContext,那这里的是什么地方传入的呢?继续看
在NativeModuleRegisterBuilder.java中
构造函数中
public NativeModuleRegistryBuilder( ReactApplicationContext reactApplicationContext, ReactInstanceManager reactInstanceManager) { mReactApplicationContext = reactApplicationContext; mReactInstanceManager = reactInstanceManager; }
这里保存了调用时传入的reactContext,
接着这个文件中,有个方法
public void processPackage(ReactPackage reactPackage) { if (reactPackage instanceof LazyReactPackage) { LazyReactPackage lazyReactPackage = (LazyReactPackage) reactPackage; List<ModuleSpec> moduleSpecs = lazyReactPackage.getNativeModules(mReactApplicationContext); Map<String, ReactModuleInfo> reactModuleInfoMap = lazyReactPackage.getReactModuleInfoProvider().getReactModuleInfos(); for (ModuleSpec moduleSpec : moduleSpecs) { String className = moduleSpec.getClassName(); ReactModuleInfo reactModuleInfo = reactModuleInfoMap.get(className); ModuleHolder moduleHolder; if (reactModuleInfo == null) { NativeModule module; ReactMarker.logMarker( ReactMarkerConstants.CREATE_MODULE_START, moduleSpec.getClassName()); try { module = moduleSpec.getProvider().get(); } finally { ReactMarker.logMarker(ReactMarkerConstants.CREATE_MODULE_END); } moduleHolder = new ModuleHolder(module); } else { moduleHolder = new ModuleHolder(reactModuleInfo, moduleSpec.getProvider()); } String name = moduleHolder.getName(); putModuleTypeAndHolderToModuleMaps(className, name, moduleHolder); } } else { FLog.d( ReactConstants.TAG, reactPackage.getClass().getSimpleName() + " is not a LazyReactPackage, falling back to old version."); List<NativeModule> nativeModules; if (reactPackage instanceof ReactInstancePackage) { ReactInstancePackage reactInstancePackage = (ReactInstancePackage) reactPackage; nativeModules = reactInstancePackage.createNativeModules( mReactApplicationContext, mReactInstanceManager); } else { nativeModules = reactPackage.createNativeModules(mReactApplicationContext); } for (NativeModule nativeModule : nativeModules) { addNativeModule(nativeModule); } } }
有个代码 nativeModules = reactPackage.createNativeModules(mReactApplicationContext);
这里终于有点熟悉了,我们在平时给rn添加模块时都会写一个XXXpackage.java,里面有个方法
@Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new ReactBridge(reactContext)); return modules; }
看到了吧,我们平时在自己写的模块里面用的reactContext来自上面 reactPackage.createNativeModules(mReactApplicationContext);
传入的,这个代码是在NativeModuleRegisterBuilder.java中的,这个类保存了它被调用时传入的context,那它在哪里被调用的呢?
答案是在ReactInstanceManager.java中,
这个会传入reactContext,它来自于同一个ReactInstanceManager.java文件中的
是new出来的,他new出来后被方法返回了,这个方法再这里有调用
从名字可以看出,是在线程中创建reactContext的,里面会执行
此刻真正的reactContext被创建了,它会传入这个方法setupReactContext
此刻这个ReactInstanceManager的这个mCurrentReactContext终于有值了,并且他是 private @Nullable volatile ReactContext mCurrentReactContext;
ReactContext类型,那我们记得我们上面提到的疑问1的地方,那里的mCurrentReactContext 就是这里的,他俩一样,那何时给他绑定activity呢
看这里还是那个ReactInstanceManager.java文件
我们可以看到onHostResume中传入了一个activity,之后复制给mCurrentActivity,这个mCurrentActivity会在getCurrentActivity中返回出去,
这个方法定义在ReactContext中,而ReactApplicationContext 继承自ReactContext,所以ReactApplicationContext 的实例mCurrentReactContext就有了这个方法,那关键就在onHostResume在什么地方调用的时候传入了当前activity呢
答案是在ReactActivityDelegate.java中
protected void onResume() { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onHostResume( getPlainActivity(), (DefaultHardwareBackBtnHandler) getPlainActivity()); } if (mPermissionsCallback != null) { mPermissionsCallback.invoke(); mPermissionsCallback = null; } }
我们看到这里传入了getPlainActivity(),这个方法
private Activity getPlainActivity() { return ((Activity) getContext()); }
这个返回的是当前context绑定的activity,我们知道ReactActivity是把自己交给了ReactActivityDelegate.java这个代理类,所以在这个代理类中得到的activity就是你那个继承ReactActivity的类,比如MainActivity,到了这里终于找到了。
总结一下:
大致流程是reactactivity代理类在onHostResume中调用getReactInstanceManager().onHostResume方法传入activity,而后被赋值为ReactInstanceManager类实例的私有变量mCurrentActivity.
由于我不是安卓开发,所以以上理解如有不对请提示
4. 2018.10.28 解决createSwitchNavigator 无法使用转场动画问题
开发时用到了createSwitchNavigator 但是这个导航器不支持过渡动画,因为它可以实现认证流程,也就是登录后按返回没法返回登录页面,但是没动画就很难受,社区很多人在提这个需求,具体可以看这里 switchnavigator-animate-between-screens
很多人要求加这个功能,我感觉他们实现起来不难啊,为什么到了现在还在计划中,但是以后应该会有这个功能吧,目前没有,这个文章回复里面也有几个解决方案,我试了这个https://www.npmjs.com/package/react-navigation-switch-transitioner
但是我运行的时候有报错,提了issues,我最终放弃了,但是我想到了一个解决方法
比如我们在Login.tsx页面
componentDidMount(){ this. didBlurSubscription = this.props.navigation.addListener( 'didBlur', payload => { console.log('Login blur') const resetActions=StackActions.reset({ index:0, actions:[NavigationActions.navigate({routeName:"Register"})] }) this.props.navigation.dispatch(resetActions) } ); } componentWillUnmount(){ console.log('Login unmount') this.didBlurSubscription.remove(); }
上面的代码中我们在页面失去焦点的时候reset当前路由state,使得只剩下一个Register页面,这样按返回键时就直接退出了。
我们还要创建createStackNavigator因为只有它支持动画啊
顺便贴下修改react-navigation页面转场动画的代码
import {createStackNavigator, StackViewTransitionConfigs} from 'react-navigation'; const LoginStack=createStackNavigator({ Main:{ screen:LoginHome }, Register:{ screen:Register } }, { initialRouteName:"Main", transitionConfig:()=>StackViewTransitionConfigs.SlideFromRightIOS, navigationOptions:{ header:null } })
这样就改成了从右向左进入的方式。
5.2018.10.29更新,使用react-native-splash-screen解决安卓RN白屏
用react-native开发安卓会有白屏问题,其实是有两个白屏阶段,第一个是安卓自己启动白屏,第二个是RN代码初始化时的白屏问题,RN白屏我们可以用react-native-splash-screen这个插件解决,一般大部分人觉得是不是彻底没有白屏了?其实不是,还有安卓自己启动的白屏也要解决下面说下办法:
1.
首先我们在安卓res文件夹的values或者其他values-vXX,在里面找到styles.xml,没有的话新建一个,内容如下
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> </style> <!--这里我们定义一个主题样式,叫startTheme然后设置windowBackground属性--> <style name="startTheme" parent="AppTheme"> <item name="android:windowBackground">@mipmap/launch_screen</item> </style> </resources>
在这之前我们要在res里面添加mipmap文件夹或者drawable文件夹类似这样
这样上面的样式就能引用这个图片了,也就是开屏页的图片,
然后我们在我们的AndroidManifest.xml中找到我们RN所在的MainActivity在上面添加刚才的样式
此时如果运行APP,你点击后应该毫秒级别出现你刚才添加的图片,接着安卓启动完后会启动RN的渲染,RN在我们组件渲染完成前借助
react-native-splash-scree 会展示一个图片,一般建议这个图片跟我们样式的图片一致,这样用户感觉不到是两个过程。
接下来还有东西要处理,我们要理解一下那个
<style name="startTheme" parent="AppTheme"> <item name="android:windowBackground">@mipmap/launch_screen</item> </style>
我设置它后,它在我的整个MainActivity都设置了个背景图,以至于我的RN界面默认是这个背景图,所以我们要设置RN界面默认背景白色
这样MainActivity背景色就改成白色了,而不是刚才那个背景图了
OK,问题完美解决
6.2018.11.3更新,使用webstrom调试react-native
平时用谷歌浏览器调试代码的时候,输入 debugger 这种调试方法有时候在谷歌浏览器看到的断点位置跟实际断点位置不是很一致,所以最近又重新研究了下使用webstrom直接打红点断点来调试。
1: 首先建议使用android studio在手动安装debug App,也就是下图箭头所指的位置,使用这个安装,报错了比直接用命令行运行好排查问题,有时候用react-native run-android总是报错,但是用as运行就没问题,很奇怪的问题
2:等app安装成功后,接下来要配置webstrom
在webstrom工具栏顶部找到这个东西,一般在右上角
点击Edit Configurations
打开后点击+好找到ReactNative
点击OK
出现这个界面
3.然后我再建议加个这个配置
点击 ... 出现下面的弹窗
进行图中的操作
这个是让谷歌浏览器静默运行 不弹出那个debug 的浏览器窗口的
- 最后运行那个小虫子
会出现这个界面,同时你在命令行运行 yarn start 跑起来packger服务
最后你还要打开rn的debug菜单
打开debug JS
然后你就可以在编辑器打红色断点了
7: 2018.11.24 android studio 打包报错 Multiple dex files define Lorg/devio/rn/splashscreen/BuildConfig
这个错尼玛找了很多方法没解决,之前也有一个 Multiple dex 的错,那个错是包重复引入的问题,但是这个查看引用BuildConfig的只有一个包引入了,并没有重复,后来开始瞎加配置起来了,突然加了这个配置就可以打包了
在app 下build.gradle中andriod 配置项添加这个就打包成功了
dexOptions { preDexLibraries = false }
8:2018.12.16 解决某些安卓手机使用StatusBar设置沉浸式失效的问题
在我自己的小米6手机上偶然发现一个bug,就是我第一次打开app后,不杀掉进程,按返回键退出,当我再次打开app后我设置的沉浸式状态栏失效了,具体表现看下图:
第一次打开时的正常样式:
可以看到内容是沉浸到状态栏下面了
当我按物理返回按钮退出后再次点击app启动后是这样子:
尼玛,沉浸式没了!
研究了一下午最后才想到是不是我在代码里面设置状态栏沉浸式没有生效?
最后看了看那react-native的StatusBarModule.java中的代码发现有个判断activity是否存在的判断
后来测试发现确实是第二次打开就会走activity==null的判断,然后直接return不再设置状态栏的样式了,
可是为什么activity是null呢?
我们通过研究react-native的源码可以知道rn会在mainActivity走到onResume生命周期时才给getCurrentActivity返回的activity设置值为MainActivity,也就是MainActivity完全可见时,那我们RN的代码初始化是从onCreate就开始了,会不会是RN的代码执行太快了,等执行到我们在js代码调用StatusBarModule的时候mainActivity此时并没有执行onResume里面的代码呢?应该是有可能的,所以在渲染react-native组件的时候我们这样写的组件
<View style={{flex:1}}> <StatusBar translucent={true} backgroundColor="rgba(0,0,0,0)" barStyle={store.statusBarColor}/> <RouterView ref={this.setNavigator} /> </View>
它渲染的时候我们rnModule里面还无法获取到activity,那我们只能加个延时了,等onResume执行后再重新设置状态栏
我们在app组件的componentDidMount中加个延时
setTimeout(()=>{ StatusBar.setTranslucent(true); StatusBar.setBackgroundColor("rgba(0,0,0,0)") },10)
这样就解决这个问题了,但是有个隐患,到底加多少的延时呢?万一有些手机很卡唉,目前还不知道这个问题如何处理,暂且加个10ms吧