クラス名の文字列を使わずに別ModuleのActivityを呼び出す

# はじめに

[クラス名の文字列を使わずにDynamic Feature ModuleのActivityを呼び出す \- Qiita](https://qiita.com/Tsuyoshi_Murakami/items/7c949192bc98681581ac) を参考に、普通のモジュール構成でこの `activity-alias` のテクニックを使うにはどうすればいいのか検証してみました。


# 対象アプリのモジュール構成

巨大でモノリシックな app のみで構成されたアプリから `legacy` モジュールや比較的独立性の高いモジュールをコツコツ分離しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39221/bd0bc916-7e1a-974d-8989-40f679935e26.png)

ゴチャゴチャしていますが今回考える対象は `config` と `login` モジュールだけです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39221/050c7d3d-ad97-4606-3043-f737a613197f.png)

この某アプリでは、`login` モジュールに存在する「ログイン画面」(`LoginActivity`)のメニューから、`config` モジュールに属する「設定画面」(`ConfigActivity`)を開くことができます。

| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/39221/88f26012-b628-25ec-8362-6280c8a7752b.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/39221/eae53ced-1ea1-ae9e-2cf4-f83dda894b45.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39221/27fa49df-e942-73f8-87ff-e9aa4642a760.png)

この設定画面を開くコードは下記のようになっています。

```java:modules/login/src/main/java/com/twitpane/login/ui/LoginActivity.java
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) {

            final Intent intent;
            try {
                intent = new Intent(this, Class.forName("com.twitpane.config.ui.ConfigActivity"));
                startActivityForResult(intent, REQUEST_SETTINGS);
            } catch (ClassNotFoundException ignored) {
            }

            return true;
        }
```

別モジュールの `Activity` なので `Class.forName()` でクラス名を FQDN で指定しています。当然ながら補完は効かず、Activityのクラス名やパッケージ変更時にリファクタリングで追随してくれません。モジュール分離のような作業ではパッケージ変更が数多く発生するので、割と致命的です(何度もやらかしました)。

# activity-alias を導入する

`config` `login` の両モジュールから見える `core` モジュールに `activity-alias` となるクラスを用意します。

```kotlin:modules/core/src/main/java/com/twitpane/core/ActivityAlias.kt
package com.twitpane.core

sealed class ConfigActivityAlias
```

`sealed class`として定義しているのは、インスタンス化を防ぐためです。

`LoginActivity` から呼び出す部分には
`ConfigActivityAlias` を指定することで Class.forName が不要になります。やったね！(しかも try-catch も不要になっています。Kotlin なら元から不要だけど)。

```java:modules/login/src/main/java/com/twitpane/login/ui/LoginActivity.java
...
            final Intent intent = new Intent(this, ConfigActivityAlias.class);
            startActivityForResult(intent, REQUEST_SETTINGS);
...
```

そして肝心の `activity-alias` を `config` モジュールの `AndroidManifest.xml` に追記します。

```xml:modules/config/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.twitpane.config">

    <application>
        <activity
            android:name="com.twitpane.config.ui.ConfigActivity" ... />

        <activity-alias
            android:name="com.twitpane.core.ConfigActivityAlias"
            android:targetActivity="com.twitpane.config.ui.ConfigActivity" />

    </application>
</manifest>
```

これで実行してみると、ログイン画面から設定画面を無事呼び出すことができました。

ちなみに logcat には下記のように `ConfigActivityAlias` のほうが出力されていました。

```
02-22 21:59:40.099 780-3022/? I/ActivityManager: START u0 {cmp=com.twitpane.debug/com.twitpane.core.ConfigActivityAlias} from uid 10083 on display 0
02-22 21:59:40.402 780-801/? I/ActivityManager: Displayed com.twitpane.debug/com.twitpane.core.ConfigActivityAlias: +275ms
```

実運用では、[クラス名の文字列を使わずにDynamic Feature ModuleのActivityを呼び出す \- Qiita](https://qiita.com/Tsuyoshi_Murakami/items/7c949192bc98681581ac) で解説されているように、ProGuard/R8でリネームされないように、アプリケーションの`proguard-rules.pro`に設定を追加すべきです。お忘れなく。

マルチモジュール化が捗りそうですね。

以上でございます。