RN集成到現有Android原生應用

將React Native集成到現有的Android應用中需要如下幾個主要步驟:

  • 首先,你需要有一個React Native項目;
  • 為已存在的Android應用添加React Native所需要的依賴;
  • 創建index.js并添加你的React Native代碼;
  • 創建一個Activity來承載React Native,在這個Activity中創建一個ReactRootView來
  • 為React Native服務的容器;
  • 啟動React Native的Packager服務,運行應用;
  • 運行、打包;

1、創建一個React Native項目

在做混合開發之前我們首先需要創建一個沒有Android和iOS模塊的React Native項目。我們可以通過兩種方式來創建一個這樣的React Native項目:

  1. 通過npm安裝react-native的方式添加一個React Native項目;
  2. 通過react-native init來初始化一個React Native項目;

1.通過npm安裝react-native的方式添加一個React Native項目

第一步:創建一個名為RNApp的目錄,然后在該目錄下添加一個包含如下信息的package.json:

{
  "name": "RNApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}

第二步:在為package.json添加react-native
在該目錄下執行:

npm install --save react-native

執行完上述命令之后,你會看到如下警告:

image

其中,有一條警告npm WARN react-native@0.57.5 requires a peer of react@16.6.1 but none is installed.告訴我們需要安裝react@16.6.1

npm install --save react@16.6.1

至此,一個不含Android和iOS模塊的React Native項目便創建好了。

2. 通過react-native init來初始化一個React Native項目;

除了上述方式之外,我們也可以通過react-native init命令來初始化一個React Native項目。

react-native init RNApp

上述命令會初始化一個完成的名為RNApp的React Native項目,然后我們將里面的android和ios目錄刪除,替換成已存在Android和iOS項目。

2. 添加React Native所需要的依賴

在上文中我們已經創建了個一個React Native項目,接下來我們來看一下如何將這個React Native項目和我們已經存在的Native項目進行融合。

在進行融合之前我們需要將已經存在的Native項目放到我們創建的RNApp下,比如:我有一個名為MyAndroidTest的Android項目,將其放到RNApp目錄下:


第一步:配置maven

接下來我們需要為已經存在的MyAndroidTest項目添加 React Native依賴,在RNApp/MyAndroidTest/app/build.gradle文件中添加如下代碼:

dependencies {
    implementation 'com.android.support:appcompat-v7:23.0.1'
    ...
    implementation "com.facebook.react:react-native:+" // From node_modules
}

然后,我們為MyAndroidTest項目配置使用的本地React Native maven目錄,在RNApp/MyAndroidTest/build.gradle文件中添加如下代碼:

allprojects {
    repositories {
        maven {
            // All of React Native (JS, Android binaries) is installed from npm
            url "$rootDir/../node_modules/react-native/android"
        }
        ...
    }
    ...
}

提示:為確保你配置的目錄正確,可以通過在Android Studio中運行Gradle sync 看是否有 “Failed to resolve: com.facebook.react:react-native:0.x.x” 的錯誤出現,沒有錯誤則說明配置正確,否則說明配置路由有問題。

第二步:配置權限

接下來我們為APP運行配置所需要的權限:檢查你項目中的AndroidManifest.xml文件中看是否有如下權限:

<uses-permission android:name="android.permission.INTERNET" />

如果沒有,則需要將上述權限添加到AndroidManifest.xml中。

另外,如果需要訪問 DevSettingsActivity界面(即開發者菜單),

則還需要在 AndroidManifest.xml 中聲明:

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

提示:上述圖片就是RN 開發調試彈框中的Dev Settings功能,打開該功能會彈出上圖的一個界面,這個界面就是DevSettingsActivity。

第三步:指定要ndk需要兼容的架構(重要)

Android不能同時加載多種架構的so庫,現在很多Android第三方sdks對abi的支持比較全,可能會包含armeabi, armeabi-v7a,x86, arm64-v8a,x86_64五種abi,如果不加限制直接引用會自動編譯出支持5種abi的APK,而Android設備會從這些abi進行中優先選擇某一個,比如:arm64-v8a,但如果其他sdk不支持這個架構的abi的話就會出現crash。如下圖:

所以需要在app/gradle 文件中添加如下代碼:

defaultConfig {
....?
    ndk {
        abiFilters "armeabi-v7a", "x86"
    }
}

上述代碼的意思是,限制打包的so庫只包含armeabi-v7ax86

3.創建index.js并添加你的React Native代碼

通過上述兩步,我們已經為MyAndroidTest項目添加了React Native依賴,接下來我們來開發一些JS代碼。

在RNApp目錄下創建一個index.js文件并添加如下代碼:

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('RNApp', () => App);

上述代碼,AppRegistry.registerComponent('RNApp', () => App);目的是向React Native注冊一個名為RNApp的組件

另外,在上述代碼中我們引用了一個App.js文件:

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          這個是RN界面
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  }
 });

這個App.js文件代表了我們React Native的一個頁面

以上就是為本次演示所添加的React Native代碼,你也可以根據需要添加更多的React Native代碼以及組件出來。

4. 為React Native創建一個Activity來作為容器

經過上述3、4步,我們已經為MyAndroidTest項目添加了React Native依賴,并且創建一些React Native代碼和注冊了一個名為RNApp的組件,接下來我們來學習下如何在MyAndroidTest項目中使用這個RNApp組件。

為React Native創建一個Activity來作為容器,有兩種方法

  • 使用RNPageActivity來作為RN容器
  • 使用ReactPageActivity來作為RN容器

使用RNPageActivity來作為RN容器

首先我們需要創建一個Activity來作為React Native的容器,

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.shell.MainReactPackage;

public class RNPageActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // 這個"RNApp"名字一定要和我們在index.js中注冊的名字保持一致AppRegistry.registerComponent()
        mReactRootView.startReactApplication(mReactInstanceManager, "RNApp", null);

        setContentView(mReactRootView);
    }

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

參數說明

  • setBundleAssetName:打包時放在assets目錄下的JS bundle包的名字,App release之后會從該目錄下加載JS bundle;
  • setJSMainModulePath:JS bundle中主入口的文件名,也就是我們上文中創建的那個index.js文件;
  • addPackage:向RN添加Native Moudle,在上述代碼中我們添加了new MainReactPackage()這個是必須的,另外,如果我們創建一些其他的Native Moudle也需要通過addPackage的方式將其注冊到RN中。需要指出的是RN除了這個方法外,也提供了一個addPackages方法用于批量向RN添加Native Moudle;
  • setUseDeveloperSupport:設置RN是否開啟開發者模式(debugging,reload,dev memu),比如我們常用開發者彈框;
  • setInitialLifecycleState:通過這個方法來設置RN初始化時所處的生命周期狀態,一般設置成LifecycleState.RESUMED就行,和下文講的Activity容器的生命周期狀態關聯;
  • mReactRootView.startReactApplication:它的第一個參數是mReactInstanceManager,第二個參數是我們在index.js中注冊的組件的名字,第三個參數接受一個Bundle來作為RN初始化時傳遞給JS的初始化數據。

在中AndroidManifest.xml注冊一個RNPageActivity

Android系統要求,每一個要打開的Activity都要在·AndroidManifest.xml·中進行注冊:

<activity
    android:name=".RNPageActivity"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="adjustResize"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar" />

上述代碼中我們為RNPageActivity添加了一個@style/Theme.AppCompat.Light.NoActionBar類型的theme,這也是React Native UI組件所要求的主題。

為ReactInstanceManager添加Activity的生命周期回調

一個 ReactInstanceManager可以被多個activitiesfragments共享,所以我們需要在Activity的生命周期中回調ReactInstanceManager的對于的方法。

 @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
public void onBackPressed() {
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onBackPressed();
    } else {
        super.onBackPressed();
    }
}

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

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostDestroy(this);
    }
    if (mReactRootView != null) {
        mReactRootView.unmountReactApplication();
    }
}

從上述代碼中你會發現有個不屬于Activity生命周期中的方法onBackPressed,添加它的目的主要是為了當用戶單擊手機的返回鍵之后將事件傳遞給JS,如果JS消費了這個事件,Native就不再消費了,如果JS沒有消費這個事件那么RN會回調invokeDefaultOnBackPressed代碼。

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

添加開發者菜單

在RN中有個很好用的工具開發者菜單,我們平時調試RN應用時對它的使用頻率很高,接下來我們來為MyAndroidTest添加開著菜單。

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {//Ctrl + M 打開RN開發者菜單
        mReactInstanceManager.showDevOptionsDialog();
        return true;
    }
    return super.onKeyUp(keyCode, event);
}

通過上代碼即可監聽Ctrl + M來打開RN開發者菜單。

使用ReactPageActivity來作為RN容器

在上述的代碼中我們都是通過ReactInstanceManager來創建和加載JS的,然后重寫了Activity的生命周期來對ReactInstanceManager進行回調,另外,重寫了onKeyUp來啟用開發者菜單等功能。

另外,查看RN的源碼你會發現在RN sdk中有個叫ReactActivity的Activity,該Activity是RN官方封裝的一個RN容器。另外,在通過react-native init命令初始化的一個項目中你會發現有個MainActivity是繼承ReactActivity的,接下來我們就來繼承ReactActivity來封裝一個RN容器。

package com.zxj.myandroidtest;

import com.facebook.react.ReactActivity;

public class ReactPageActivity extends ReactActivity{
    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "RNApp";
    }
}

另外,我們需要實現一個MainApplication并添加如下代碼:

package com.zxj.myandroidtest;

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.<ReactPackage>asList(
          new MainReactPackage()
      );
    }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

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

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}

上述代碼的主要作用是為ReactActivity提供ReactNativeHost,查看源碼你會發現在ReactActivity中使用了ReactActivityDelegate,在ReactActivityDelegate中會用到MainApplication中提供的ReactNativeHost

 protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}

另外實現了MainApplication之后需要在AndroidManifest.xml中添加MainApplication

以上就是通過繼承ReactActivity的方式來作為RN容器的。

那么這兩種方式各有什么特點:

  • 通過ReactInstanceManager的方式:靈活,可定制性強;
  • 通過繼承ReactPageActivity的方式:簡單,自帶開發者菜單,可定制性差;

5. 運行React Native

經過上述的步驟,我們已經完成了對一個現有Android項目MyAndroidTest添加了RN,并且通過兩種方式分別創建了一個RNPageActivityReactPageActivity的Activity來加載我們在JS中注冊的名為RNApp的RN 組件。

我們先打開AndroidStudio,在MainActivity中添加一個跳轉到RN界面的按鈕

public class MainActivity extends AppCompatActivity {

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

    public void onSkipClick(View view) {
        startActivity(new Intent(this,RNPageActivity.class));
    }
}

接下來我們來啟動RN服務器,運行MyAndroidTest項目打開RNPageActivityReactPageActivity來查看效果:

npm start

在RNApp的根目錄運行上述命令,來啟動一個RN本地服務:

保持 packager 的窗口運行不要關閉,然后像往常一樣編譯運行你的 Android 應用(在命令行中執行./gradlew installDebug或是在 Android Studio 中編譯運行)。

編譯執行一切順利進行之后,在進入到 RNPageActivity或者ReactPageActivity 時應該就能立刻從 packager 中讀取 JavaScript 代碼并執行和顯示:

gif

6、打包

雖然,通過上述步驟,我們將RN和我們的MyAndroidTest項目做了融合,但打包MyAndroidTest你會發現里面并不包含JS部分的代碼,如果要將JS代碼打包進Android Apk包中,可以通過如下命令:

react-native bundle --platform android --dev false --entry-file index.js --bundle-output MyAndroidTest/app/src/main/assets/index.android.bundle --assets-dest MyAndroidTest/app/src/main/res/

參數說明

  • --platform android:代表打包導出的平臺為Android;
  • --dev false:代表關閉JS的開發者模式;
  • -entry-file index.js:代表js的入口文件為index.js;
  • --bundle-output:后面跟的是打包后將JS bundle包導出到的位置;
  • --assets-dest:后面跟的是打包后的一些資源文件導出到的位置;

注意:把上述命令中的路徑替換為你實際項目的路徑。如果 assets 目錄不存在,則需要提前自己創建一個。
提示:JS bundle一定要正確放到你的Android言語的assets目錄下,這個和我們上文中配置的setBundleAssetName("index.android.bundle")進行對應。

上面命名運行成功后會在assets目錄下生成一個index.android.bundle文件,然后在使用AS進行打包就可以了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,661評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,788評論 2 59
  • 期待已久的新課上線啦!解鎖React Native開發新姿勢,一網打盡React Native最新與最熱技術,點我...
    CrazyCodeBoy閱讀 20,297評論 3 32
  • 杜牧寫《贈別》:“娉娉裊裊十三余,豆蔻梢頭二月初。春風十里揚州路,卷上珠簾總不如。就是下面這種植物。其實早在公元5...
    冰眉鐵面閱讀 415評論 2 1
  • 嘴里含著小半碗隔夜的涼茶,“噗”的一口,盡數吐在剛剛磨好的柴刀刀刃上,拿袖口隨意擦拭了下嘴角水漬,小三百用恰好余老...
    李怪怪閱讀 419評論 0 3