
React Nativeで複数のアプリを共存させられるか試してみた
Posted on
こんにちは、エンジニアの中間です。
普段はKARTEのネイティブアプリ向けSDKの開発をしています。
React Nativeで遊んでみたことを紹介します。
こちらの投稿はPLAID Advent Calendar 2018の6日目の記事でもあります。
KARTEではWebサイトやアプリを通してユーザ一人一人に合わせたコミュニケーションをリアルタイムに取ることを大事にしています。
例えばKARTEのSDKは、管理画面でHTML/CSS/JavaScriptで記述されたアクションをアプリ内メッセージ(ポップアップやバナー、アンケート等)として表示する機能を持っています。
アプリ内メッセージの見た目や挙動を柔軟にカスタマイズできるので、様々な訴求を行えます。
今回は、よりリッチでネイティブライクな見た目のメッセージを出せないか、という可能性を探るために、React Nativeを使いアプリ内メッセージのような機能の実装・実験をします。
ポイントは以下の点です。
- 主となるアプリ上に、アプリ内メッセージを模した第二のアプリを読み込み表示できるか
- 第二のアプリをリアルタイムに(アプリアップデートの必要なく)変更できるかどうか
概要
以下の構成のサンプルアプリを作り正常に起動・表示できるかを試します。
- メインのReact Nativeアプリの上にサブのReact Nativeアプリを表示する。サブのアプリではサーバからfetchしたbundleを使う(プロダクションを想定し、DeveloperSupportは使わない)
通常React Nativeのアプリをリリース用にビルドするとassetsにbundleファイルが配置されますが、ReactInstanceManager.setBundleLoader
によりbundleの読み込み方法は自由に指定できるようです。
メインのアプリは通常通りassetsに配置し、サブのアプリはサーバからInternal Storageにダウンロードしたbundleを使います。
今回はAndroidのみで試しました。
React Nativeプロジェクト
以下のようにプロジェクトを用意し、index.jsをメインのアプリ、index.sub.jsをサブのアプリとします。
├── android/ (Androidのプロジェクト)
├── index.js
├── index.sub.js
├── node_modules/
└── package.json
index.jsはMain App
の文字列を中央に表示するだけのアプリです。index.sub.jsも同じように、少しだけ見た目を変えたものを用意します。
class HelloWorld extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello}>Main App</Text>
</View>);
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
hello:{
fontSize: 30,
textAlign: 'center',
},
});
設定
build.gradle
のproject.ext.react.entryFile
にindex.js
を指定します。これによりindex.jsのビルドファイルがassets/下にindex.android.bundleとして配置されます。
project.ext.react = [
entryFile: "index.js",
bundleInDebug: true,
bundleAssetName: "index.android.bundle"
]
index.sub.js
は手動でビルドし、サーバから配布します。ビルドファイルはindex.sub.bundle.js
という名前にします。
> react-native bundle --platform android --entry-file index.sub.js --bundle-output index.sub.bundle.js --dev false
> http-server ./ #https://www.npmjs.com/package/http-server
Androidアプリの実装
Androidアプリは単一のActivityを持ち、Activity上に二つのReactRootView
とReactInstanceManager
を保持して、二つのReactNativeアプリを読み込む構成にします。サブアプリはメインアプリの上に小さく重ねて表示させます。
public class TopActivity extends AppCompatActivity {
public static final String SUB_BUNDLE_URL = "http://172.16.1.58:8080/index.sub.bundle.js";
private ReactRootView mainReactRootView;
private ReactRootView subReactRootView;
private ReactInstanceManager mainReactInstanceManager;
private ReactInstanceManager subReactInstanceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_top);
final FrameLayout rootLayout = findViewById(R.id.root_layout);
//mainReactInstanceManagerのセットアップ
mainReactRootView = new ReactRootView(this);
mainReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")
.addPackage(new MainReactPackage())
.setCurrentActivity(this)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
mainReactRootView.startReactApplication(mainReactInstanceManager, "MainReactNativeApp");
//メインAppは全画面表示
rootLayout.addView(mainReactRootView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
//index.sub.bundle.jsをダウンロードし、subReactInstanceManagerをセットアップ
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... voids) {
return fetchSubBundle();
}
@Override
protected void onPostExecute(Boolean isSuccessful) {
if (!isSuccessful) return;
subReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setJSBundleFile(getSubAppBundleFile().getAbsolutePath())
.addPackage(new MainReactPackage())
.setCurrentActivity(TopActivity.this)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
//サブAppは画面上部に小さく表示
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 600);
params.gravity = Gravity.TOP;
params.setMargins(30, 30, 30, 0);
subReactRootView = new ReactRootView(TopActivity.this);
subReactRootView.startReactApplication(subReactInstanceManager, "SubReactNativeApp");
subReactRootView.setLayoutParams(params);
rootLayout.addView(subReactRootView);
}
}.execute();
}
private File getSubAppBundleFile(){
return new File(TopActivity.this.getFilesDir(), "index.android.sub.bundle");
}
private boolean fetchSubBundle() {
try {
ResponseBody body = new OkHttpClient()
.newCall(new Request.Builder().url(SUB_BUNDLE_URL).build())
.execute()
.body();
if (body == null) return false;
BufferedInputStream input = new BufferedInputStream(body.byteStream());
OutputStream output = new FileOutputStream(getSubAppBundleFile());
byte[] data = new byte[1024];
int count;
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (IOException e) {
return false;
}
return true;
}
@Override
protected void onPause() {
super.onPause();
mainReactInstanceManager.onHostPause(this);
if (subReactInstanceManager != null) {
subReactInstanceManager.onHostPause(this);
}
}
//onResume, onDestroyも同様に両方のmanagerにdispatchする
アプリを起動すると、正しくメインアプリの上にサブアプリが重なって表示されるのを確認できました。

ReactInstanceManager
のインスタンス毎にJSのコンテキストが作られるようで、実際、index.jsでglobalに定義した変数をindex.sub.jsからは参照できませんでした。
実際に活用するためには考えるべきことがまだまだ沢山ありますが、複数のReactNative アプリの共存を動かして試すことができました!
今回作ったサンプルアプリは以下にあげました。
https://github.com/takaaki7/MultiReactNativeApp
最後に
CX(顧客体験)プラットフォーム「KARTE」を運営するプレイドでは、KARTEを使ってこんなアプリケーションが作りたい! KARTE自体の開発に興味がある!というエンジニア(インターンも!)を募集しています。
詳しくは弊社採用ページまたはWantedlyをご覧ください。 もしくはお気軽に、下記の「話を聞きに行きたい」ボタンを押してください!