android 原生应用集成 react-native

一、前言

继上次尝试了《ios 原生应用集成 react-native》之后。这次尝试 android 原生应用的集成。android 也分两种语言,kotlin 和 java。这里就尝试 java 就好了,不和 ios 一样尝试两种语言。全屏模式用 ReactRootView 组件,窗口模式使用 Fragment 组件做承载。有没有点眼熟,这个在 web 里代表空标签。vue 少见点, react 应该挺常见的。也有专门的 api,document.createDocumentFragment

这里也不多说,主要是记录下官网《ntegration with Existing Apps》教程里没有的。大部分还是跟着官网走就可以。老样子,也是建立在正常的 react-native android 环境能运行的前提下,才能继续。按照官网《Setting up the development environment》说的,JDK,Android Studio 该装的装好。

还是那句话,app 开发的同学应该一看就懂了,我这还是以一个只懂前端的视角记录一下。

二、全屏集成

创建按钮很显眼,就不用多说。这里说下两个模版,第一行第二个 Empty Activity,据说是新模板,开发语言只有 kotlin。第二行第二个 Empty Views Activity,据说是旧模板,开发语言可以选 kotlin 和 java,这里选 java。至于最下面还有个选择 build 语言的,选择第一个 Kotlin DSL(build.gradle.kts) 就可以了,反正三个我都看了都和官网示例对没完全对上。

1. MyReactActivity.java

创建一份 MyReactActivity.java 文件,如果是新项目的话,会有一份 MainActivity.java,放在一起就好了。

// 记得声明自己应用
package com.example.myapplicationrn;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.KeyEvent;

import com.facebook.react.PackageList;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.soloader.SoLoader;

import java.util.List;

public class MyReactActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {

    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SoLoader.init(this, false);

        mReactRootView = new ReactRootView(getApplication());
        List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
        // 有一些第三方可能不能自动链接,对于这些包我们可以用下面的方式手动添加进来:
        // packages.add(new MyReactNativePackage());
        // 同时需要手动把他们添加到`settings.gradle`和 `app/build.gradle`配置文件中。

        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackages(packages)
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // 注意这里的MyReactNativeApp 必须对应"index.js"中的
        // "AppRegistry.registerComponent()"的第一个参数
        mReactRootView.startReactApplication(mReactInstanceManager, "reactnativeandroid", null);

        setContentView(mReactRootView);
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

    @Override
    protected void onPause() {
        super.onPause();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostPause(this);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostResume(this, this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
        }
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostDestroy(this);
            mReactInstanceManager.destroy();
        }
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }
}

其实就是官网示例,然后整合成一份了。再把一些依赖 import 写上,不然完全新手看一堆依赖缺失还是挺不好的。

2. settings.gradle.kts

import groovy.lang.Closure

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
//dependencyResolutionManagement {
//    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
//    repositories {
//        google()
//        mavenCentral()
//    }
//}

rootProject.name = "My Application Rn"
include(":app")

apply(from="../node_modules/@react-native-community/cli-platform-android/native_modules.gradle")

val applyNativeModulesSettingsGradle: Closure<Any> = extra.get("applyNativeModulesSettingsGradle") as Closure<Any>
applyNativeModulesSettingsGradle(settings)

includeBuild("../node_modules/@react-native/gradle-plugin")

主要注意点是那段注释的,是默认新建工程带的。但是据说应该是:引入了 node_modules 里的 react-native 依赖就指定了仓库的引入,所以没有注释的话,会报大概是这样的错误:
A problem occurred configuring project ‘:app’.
Build was configured to prefer settings repositories over project repositories but repository

其它的是官网示例的语句替换成的新语法写法。对于 applyNativeModulesSettingsGradle 这个方法,是参考网上大佬给的解决方法,可能会有更合理更优雅的实现。

3. build.gradle.kts

plugins {
    id("com.android.application") version "8.2.1" apply false
}

buildscript {
    dependencies {
        classpath("com.facebook.react:react-native-gradle-plugin")
    }
}

4. app/build.gradle.kts

import groovy.lang.Closure

plugins {
    id("com.android.application")
    id("com.facebook.react")
}

android {
    namespace = "com.example.myapplicationrn"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.myapplicationrn"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.9.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    // 新增这两句
    implementation("com.facebook.react:react-android")
    // 新增这两句
    implementation("com.facebook.react:hermes-android")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

apply(from="../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle")

val applyNativeModulesAppBuildGradle: Closure<Any> = extra.get("applyNativeModulesAppBuildGradle") as Closure<Any>
applyNativeModulesAppBuildGradle(project)

这个依然是按照官网的示例,替换成新语法写法。

5. AndroidManifest.xml

这个比较简单了,参考原来的增加一个 <activity android:name=".MyReactActivity"> 标签就可以,就是 android:name 属性的值要是上面自己定义的 activity 类。

三、Fragment 集成

全屏倒还好,Fragment 的集成更加花费点功夫。依然官网《Integration with an Android Fragment》先行。但是果不其然,没那么顺利,Android Fragment 集成 react-native 一点就崩

1. .MyReactApplication.java

其实是一开始不会看日志,按照官网集成之后,点击按钮弹出 Fragment,就直接闪退了。一路追踪后,有个地方要补充一下,直接看 .MyReactApplication.java

package com.example.myapplicationrn;

import android.app.Application;

import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;

import java.util.List;

public class MyReactApplication extends Application implements ReactApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        SoLoader.init(this, false);
    }

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            return BuildConfig.DEBUG;
        }

        protected List<ReactPackage> getPackages() {
            List<ReactPackage> packages = new PackageList(this).getPackages();
            // Packages that cannot be autolinked yet can be added manually here
            return packages;
        }

        // 注意注意注意添加这个方法
        @Override
        protected String getJSMainModuleName() {
            return "index";
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }
}

注意点:mReactNativeHost 里要添加多一个方法 getJSMainModuleName,就是复写父类的方法。返回文件的入口,不然默认返回的是 index.android。这就会导致服务和 app 都启动了,但是报错说服务器返回 404。如:
Unable to download JS bundle
com.facebook.react.common.DebugServerException: The development server returned response error code: 404

2. .MainActivity.java

再看 .MainActivity.java

package com.example.myapplicationrn;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.facebook.react.ReactFragment;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;


public class MainActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button mButton = findViewById(R.id.button);
        mButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {

                TextView textView = findViewById(R.id.textView);
                String newText = "WTF!";
                textView.setText(newText);

                Fragment reactNativeFragment = new ReactFragment.Builder()
                        .setComponentName("reactnativeandroid")
                        // setFabricEnabled 必须写
                        .setFabricEnabled(false)
                        // setLaunchOptions 可以省略
                        .setLaunchOptions(getLaunchOptions("test message"))
                        .build();

                getSupportFragmentManager()
                        .beginTransaction()
                        .add(R.id.frameLayout, reactNativeFragment)
                        .commit();

            }
        });

    }

    private Bundle getLaunchOptions(String message) {
        Bundle initialProperties = new Bundle();
        initialProperties.putString("message", message);
        return initialProperties;
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }
}

注意点:setFabricEnabled 这个方法必须写,闪退的原因就是这个取值为空,直接崩了。

3. AndroidManifest.xml

最后,可能还会报错:
java.lang.ClassCastException: android.app.Application cannot be cast to com.facebook.react.ReactApplication

AndroidManifest.xml 文件,application 标签加上一句:android:name=".MyReactApplication" 就好了。当然这个值也是和上面自定义的类相对应。

总结

也没啥总结的,后面来看也改得不多,但是有时就差那么一两句,就是跑不起来。熟悉相关开发的同学看看源码估计也很容易就完成。这里就权当一个学习记录吧。

其它

关于 react-native 开发相关,可以参考《react-native ios 流水账》。
与 ios 的集成,可以参考《ios 原生应用集成 react-native》。