Android製アプリからReact Native製のアプリを呼び出してさらにAndroid製のアプリを呼ぶ

やりたいこと

Android製のアプリから呼ぶReact Nativeが、別のAndroidアプリ(aar形式)を呼んでても問題ないかを検証したいです。

成果物

やりたいことのイメージ

やりたいことを図で示すとこんな感じ

頭の中のもやもや

クロスプラットフォームをうたっているReact Nativeなのでえきそうですが、私の頭の中には以下のもやもやがありました。

図は開発者が触れる形式でのファイル形式を示しています。
アプリBとアプリCはできていてそれをSDKとして配布することを考えていて、アプリAは全く知らない人が作るイメージです。そのため、アプリBとアプリCはソースコードではなくパッケージ化する必要があります。

~もやもや~
①React NativeのBundleとaarの関連 React Nativeはjsコードとネイティブコードがあるので、生成物はバンドルファイルとaarファイルの2つ。React Nativeプロジェクト内ではReact Native→Android の呼び出しができることを以下記事で確認したけど、それぞれパッケージとして分離したときにブリッジ呼びだしとかどうなる?

②アプリCのaarの依存関係はアプリA/Bどちらに持たせるか アプリCのAARはアプリBに依存関係を持たせる?アプリAに依存関係を持たせる?(どっちのbuild.gradleに書く?)

③そもそもできるの?本当に?

結論

③→できる

①React NativeのBundleとaarの関連 アプリA側で適切に呼べば、アプリBのBundleファイルとaarファイルの関係性は意識しなくて大丈夫でした。
React Nativeのブリッジに使うネイティブコードをaar化したとしても、それはもうただのaar(部品) ただし、アプリAでブリッジの依存関係をgradleに記述する必要があります。

②アプリCのaarの依存関係はアプリA/Bどちらに持たせるか
アプリAに持たせる(アプリBには持たせなくてよい)でできました。
※アプリBのネイティブコードに入れられればアプリAからアプリCを意識しなくてよいので、そちらの方がよいかも。(後日検証します)

ーーーーーーーーーー
(現状)
普通にやると以下のエラー

Direct local .aar file dependencies are not supported when building an AAR. The resulting AAR would be broken because the classes and Android resources from any local .aar file dependencies would not be packaged in the resulting AAR

ローカルMavenリポジトリにaarファイルをインストールし、プロジェクトに依存関係を追加すればできるかもしれない
ーーーーーーーーーー

やり方

Ⅰ. アプリCを作成

aar化するため、New ModuleからAndroid Libraryとして作成。パッケージ名はappCLibraryとします。

  • AppCActivity.javaを作成 アプリCの起動用のアクティビティ
package com.example.appclibrary;

import android.os.Bundle;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class AppCActivity extends AppCompatActivity {

    private TextView textView;
    private boolean buttonTap = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
  • aar化 ライブラリのルート(/appCLibrary)
./gradlew :appCLibrary:build

Ⅱ. アプリBを作成

  • ネイティブ部分のライブラリを用意 パッケージ名はappBLibrary。手順はappCと同様。

ビルド時にエラーにならないようにするため、build.gradleに以下を追加

implementation "com.facebook.react:react-native:+" // From node_modules

com.facebook.react:react-native:を使えるようにするためにreact nativeプラグインが必要となるため、その方法については下記記事参照。 teisyoku-tabetai.hatenablog.com

  • appBLibraryのjava.com.appblibraryにModuleとPackageを追加

  • CallAppCModule.java

package com.example.appBLibrary;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.content.Intent;

public class CallAppCModuleextends ReactContextBaseJavaModule {

    private static final String REACT_CLASS = "CallAppC";
    private final ReactApplicationContext reactApplicationContext;

    public AndroidScreenModule(ReactApplicationContext reactApplicationContext) {
        super(reactApplicationContext);
        this.reactApplicationContext = reactApplicationContext;
    }

    @Override
    public String getName() {
        return "CallAppC";
    }

    @ReactMethod
    public void moveAndroidScreen() throws InterruptedException {
        Intent intent = new Intent();
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setClassName(reactApplicationContext, "com.example.appclibrary.AppCActivity "); // アプリCの起動用モジュールを設定
        reactApplicationContext.startActivity(intent);
    }
}
  • CallAppCPackage.java
package com.example.appBLibrary;

import com.facebook.react.ReactPackage;
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 CallAppCPackage 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 CallAppCModule(reactContext));
        return modules;
    }
}
  • AAR化
./gradlew :appBLibrary:build
  • App.tsxを用意
import React from 'react';
import { Text, Button, NativeModules, View, StyleSheet } from 'react-native';

const { CallAppC } = NativeModules;

export default function App() {

  const onPressExampleButton = () => {
    CallAppC.moveAndroidScreen();
  };

  

  return (
    <View style={styles.container}>
      <Text style={styles.text}>アプリB</Text>
      <Text style={styles.text}>React Native</Text>
      <Button  title="アプリCへ遷移" onPress={onPressExampleButton} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 30,
  },
})
  • バンドル化
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

Ⅲ. アプリAを作成

MyApplicationというプロジェクトで作成

  • MainAcivity.java 起動用のクラス。
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView textView;

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

        Button button = findViewById(R.id.button);

        textView = findViewById(R.id.text_view);

        button.setOnClickListener( v -> {
                this.onClick();
        });
    }

    public void onClick() {
        Intent intent = new Intent();
        intent.setClassName(getApplicationContext(), "com.example.appblibrary.MyReactActivity");
        startActivity(intent);
    }
}
  • MyReactNativeActivity React Native起動用のクラス
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView textView;

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

        Button button = findViewById(R.id.button);

        textView = findViewById(R.id.text_view);

        button.setOnClickListener( v -> {
                this.onClick();
        });
    }

    public void onClick() {
        Intent intent = new Intent();
        intent.setClassName(getApplicationContext(), "com.example.myapplication.MyReactActivity");
        startActivity(intent);
    }
}
  • MyReactActivity(【※重要】) React NativeのBundleファイルを登録するActivity。 ★で示したところにjsと通信するネイティブコードのPackageを追加することで、React Nativeのブリッジを使用することができます。
package com.example.myapplication;

import android.app.Activity;
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 Activity implements DefaultHardwareBackBtnHandler {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

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

        mReactRootView = new ReactRootView(this);
        List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
        // Packages that cannot be autolinked yet can be added manually here, for example:
         packages.add(new com.example.appblibrary.CallAppCPackage ());  //★ アプリBネイティブコードのパッケージを登録
        // Remember to include them in `settings.gradle` and `app/build.gradle` too.

        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackages(packages)
                .setUseDeveloperSupport(true)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // The string here (e.g. "MyReactNativeApp") has to match
        // the string in AppRegistry.registerComponent() in index.js
        mReactRootView.startReactApplication(mReactInstanceManager, "integratedAndroidScreen", null);

        setContentView(mReactRootView);
    }

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

}
  • 作成した2つのaarをapp/libs以下に配置

  • アプリAのgradleにアプリB、アプリCの依存関係を追加

implementation files('libs/appBLibrary-release.aar')
implementation files('libs/appCLibrary-release.aar')

まとめ

aarは部品と思ってしまえばよい。アプリA側でやることは以下になりそう。
- app/libsにaar配置
- build.gradleに依存関係を追記
- src/main/assets/にbundleファイルを追加
- ①バンドルフィルを呼ぶ、と②React Nativeのブリッジに使用するパッケージをPackage.addに書くためのActivityを追加(わんちゃん、これもアプリBのパッケージに含めてしまえばよい...?)