【初心者向け】Android Java を kotlin に変換して得た Tips

# はじめに
筆者はkotlinビギナーです。現在、習うより慣れろの方針で、手を動かしてkotlinをキャッチアップ中です。

今回は [先日の投稿](https://qiita.com/tak-wisteria/items/4ac6212e7f5fb78cc11e) にて作成した DIMock というクラスをkotlin化し、
そこで得た学びや気づき（個人的に印象に残ったもの）をまとめました。

## 変換方法
変換については、最初にAndroidStudioの自動変換機能を使い、ビルドエラーや実行時エラーを手動で修正しました。


# 印象に残った、学びや気づき

## javaのソースをコピーし、kotlinのソースにペーストすると、AndroidStudioがkotlinに変換してくれる
地味ですがビックリしました。kotlin化推進に対する誰かの熱き想いを感じます。

## static メソッドが使えず、シングルトン(object定義）にした
kotlin初心者としては驚きの事実。staticメソッドが使えない事が判明。
検討の結果、シングルトンにしました。
（Companion Objects 使う方式もありますが、コードがやや冗長になる為、不採用。）

```java:java
public class DIMock {
```
```kotlin:kotlin
object DIMock {
```
↑こんな感じで、「class」のでなく「object」と定義すると、シングルトンになります。
メソッド呼び出しは、javaとkotlinで違いがあります。

```java:javaからの呼び出し
DIMock.INSTANCE.registDICallback(...);
```

```kotlin:kotlinからの呼び出し
DIMock.registDICallback(...)
```

java classから参照する場合は「INSTANCE」を経由するんですね。

## javadoc -> KDoc になった

```java:javadoc
    /**
     * injectをhook{@link Mockito#doAnswer(Answer)}するモック
     */
```
```kotlin:KDoc
    /**
     * injectをhook[Mockito.doAnswer]するモック
     */
```

## kotlinは変数の初期値が無いとビルドエラー

```java:java
private static AppComponent sMock;
```
```kotlin:kotlin
private var sMock: AppComponent? = null
```

## ジェネリクスの「?」は「*」、「Object型」は「Any型」、「new」は不要

```java:java
private static HashMap<Class<?>, Object> sCustomMocks = new HashMap<>();
```
```kotlin:kotlin
private val sCustomMocks = HashMap<Class<*>, Any>()
```

## staticコンストラクターは「init」になる

```java:java
    static {
```
```kotlin:kotlin
    init {
```

## Class型の取得方法が変わる

```java:java
sMock = Mockito.mock(AppComponent.class);
```
```kotlin:kotlin
sMock = Mockito.mock(AppComponent::class.java)
```

「::class.java」って書き方が気になったので調べると、 javaのクラス型 Class と、 kotlinのクラス型 KClass があるんですね。

```java:javaでクラス型取得
        Class<DIMock> jclazz = DIMock.class;
        Class<DIMock> jclazz = DIMock.INSTANCE.getClass();
```

```kotlin:kotlinでクラス型取得
        val jclazz : Class<DIMock> = DIMock::class.java
        val jclazz : Class<DIMock> = DIMock.javaClass
        val kclazz : KClass<DIMock> = DIMock::class
```

## javaではinterface型を引数に取り匿名クラスを渡してた所を、匿名メソッド渡しにできる

```java:java：interface型の匿名クラス渡し
    public interface DICallback<T> {
        void onInject(T listener);
    }
    public static <T> void registDICallback(Class<T> clazz, DICallback<T> callback) {
        ...
    }
    DIMock.registDICallback(MainActivity.class, listener -> { ... });
```

```kotlin:kotlin：匿名メソッド渡し
    fun <T> registDICallback(clazz: Class<T>, callback: (listener :T) -> Unit) {
    }
    DIMock.registDICallback(MainActivity::class.java) { activity :MainActivity ->}
```
kotlinの場合、javaの様にinterface定義しなくて済みます。
勿論、interface定義して匿名クラスを渡す事も出来ます。
単一メソッドのinterfaceは上記、複数のメソッドなら下記を用いるとコード量が減るかもですね。

```kotlin:kotlin：interface型の匿名クラス渡し
    DIMock.registDICallback(MainActivity::class.java, object : DIMock.DICallback<MainActivity> {
        override fun onInject(listener: MainActivity) {
        }
    })
```
kotlinの匿名クラス渡しは、javaのラムダ式より少し煩雑になりますね。。。

## kotlinからkotlinへ匿名クラスをラムダ式で渡せないが、kotlinからjavaなら渡せる

・・・と、ここまで書いて気づいた。「あれ、object宣言しなくてもラムダ式で匿名クラス渡せている箇所がある・・・？」

```kotlin:kotlin：DIMock.kt
    val answer = { invocation : InvocationOnMock ->
        ...
        null
    }
    ...
    // 全injectをhookする
    for (method in IComponent::class.java.methods) {
        ...
        val hook = Mockito.doAnswer(answer).`when`<AppComponent>(sMock)
        ...
    }
```
```java:java：Mockitoの中身
public class Mockito extends ArgumentMatchers {
    public static Stubber doAnswer(Answer answer) {
        return MOCKITO_CORE.stubber().doAnswer(answer);
    }
}
public interface Answer<T> {
    T answer(InvocationOnMock invocation) throws Throwable;
}
```

answer変数は、AndroidStudioのサジェストの機能によると「(InvocationMock)->Nothing?型」でした。
明らかに「Answer型」ではないです。

実は、ここの謎が解けませんでした。kotlinからjavaへの匿名クラス渡しの場合は、javaの文法が適用されるから、ラムダ式が良い様に解釈されてOKって事なのかなぁ。

あまり深追いしません。

## 匿名メソッドの渡し方が複数ある
ちょっと不思議だったのが、匿名メソッドの渡し方が複数ある事。
どうやらメソッドの引数の最後が匿名メソッドの場合、書き方を変えられるらしい。

```kotlin:kotlin：匿名メソッド渡しのパターン
    // 1. メソッドの引数として渡す
    DIMock.registDICallback(xxx, { activity :MainActivity ->})
    // 2. 一度「)」で閉じた後に「{}」で括る
    DIMock.registDICallback(xxx) { activity :MainActivity ->}
    // 3. 匿名メソッド渡しが最後の引数では無い場合、「2」は使えない
    DIMock.registDICallback(xxx, { activity :MainActivity ->}, yyy)
```


## javaで定義したsetter/getterに、kotlinからプロパティっぽくアクセスできる

```java:java
    boolean accessible = field.isAccessible();
    field.setAccessible(true);
```
```kotlin:kotlin
    val accessible = field.isAccessible
    field.isAccessible = true
```

## Map.put が Map[] で表現できる

```java:java
sCustomMocks.put(clazz, mock);
```

```kotlin:kotlin
sCustomMocks[clazz] = mock
```


# 変換後のソースコード
変更前のソースは[「こちら」](https://qiita.com/tak-wisteria/items/4ac6212e7f5fb78cc11e#di%E3%82%92hook%E3%81%99%E3%82%8B)。

```kotlin:DIMock.kt
object DIMock {

    // original component
    private val sOriginal = DI.injector()

    /**
     * injectをhook[Mockito.doAnswer]するモック
     */
    private var sMock: AppComponent? = null

    private val sCustomMocks = HashMap<Class<*>, Any>()

    private val sCallbacks = HashMap<Class<*>, ArrayList<(listener :Any) -> Unit>>()

    /**
     * DaggerのDIの監視を開始する。
     * DIが実行された後、モックに置き換えたい場合は[DIMock.registDICallback] でコールバックを受け取り
     * 個別にfieldに代入する、とか、[DIMock.replaceMockAll]を実行すること。
     */
    init {

        // injectをhook(Mockito.doAnswer)する為の設定をする
        // AppComponentの実態クラスは、Daggerによりfinal宣言されているのでspyできない。
        sMock = Mockito.mock(AppComponent::class.java)

        val answer = { invocation : InvocationOnMock ->

            // injectが呼ばれた際に本コールバックが発生する

            /**
             * 現状、[IComponent.inject] のメソッドは、
             * 引数が1つしかないので 0:固定値 とする。
             */
            val arg : Any = invocation.getArgument(0)

            // originalのinjectを実行する
            callInject(sOriginal, arg.javaClass, arg)

            // テスト対象のクラスにコールバックする
            val list = sCallbacks[arg.javaClass]
            if (list != null) {
                for (callback in list) {
                    callback(arg)
                }
            }
            null
        }

        // 全injectをhookする
        for (method in IComponent::class.java.methods) {
            /**
             * [IComponent.inject] を一回呼んで[Mockito]に登録します。
             *
             * doAnswer().whien.inject()
             * [Mockito.doAnswer]
             * [Mockito.when]
             *
             * このタイミングで[IComponent.inject]をcallしても実際のメソッドが呼ばれる訳ではなく、
             * [Mockito]に登録されるだけとなります。
             *
             * 実際のActivity classなどでDI.injector().inject(this)がコールされたらanserが呼び出されます。
             * [DI.injector]
             * [IComponent.inject]
             */
            val hook = Mockito.doAnswer(answer).`when`<AppComponent>(sMock)
            val paramType = method.parameterTypes[0]
            callInject(hook, paramType, ArgumentMatchers.any(paramType))
        }

        DI.setAppComponent(sMock)
    }

    /**
     * injectを実行する
     */
    private fun <T> callInject(component: AppComponent, clazz: Class<T>, arg: Any?) {
        try {
            component.javaClass.getMethod("inject", clazz).invoke(component, arg)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * テストしたいクラスのinjectをhookするコールバックを登録する
     * @param clazz テスト対象のクラス
     * @param callback コールバック
     */
    fun <T> registDICallback(clazz: Class<T>, callback: (listener :T) -> Unit) {

        var list: ArrayList<(listener :T) -> Unit>? = sCallbacks[clazz] as ArrayList<(listener :T) -> Unit>?
        if (list == null) {
            list = ArrayList()
            sCallbacks[clazz] = list as ArrayList<(listener :Any) -> Unit>
        }

        list.add(callback)
    }

    /**
     * Inject対象のインスタンスのインジェクト済みインスタンスを全て[Mockito.mock]で置き換える
     * [DIMock.registCustomMock]で登録済みの場合は、そちらを優先する。
     * @param target
     */
    fun replaceMockAll(target: Any) {
        for (field in target.javaClass.declaredFields) {
            if (field.getAnnotation(Inject::class.java) == null) {
                continue
            }
            val accessible = field.isAccessible
            try {
                field.isAccessible = true
                val type = field.type
                var mock: Any? = sCustomMocks[type]
                if (mock == null) {
                    mock = Mockito.mock(type)
                }
                field.set(target, mock)
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                field.isAccessible = accessible
            }
        }
    }

    /**
     * モックをカスタムする場合は、事前に登録する
     * @param clazz mock化するクラス型
     * @param mock mockのインスタンス。[Mockito.mock]を使って作成しても良いし
     * テスト用スタブ（自作）を指定してもよい。
     */
    fun registCustomMock(clazz: Class<*>, mock: Any) {
        sCustomMocks[clazz] = mock
    }
}
```
kotlinのソースとして最適かと言われると、正直、自信が無いのが本音ではあります。
今後、継続的にレベルアップ＆ブラッシュアップしたいと思っています。

# いかがでしたでしょうか
何かのご参考になれば幸いです。