6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-09

はじめに

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

今回は 先日の投稿 にて作成した DIMock というクラスをkotlin化し、
そこで得た学びや気づき(個人的に印象に残ったもの)をまとめました。

変換方法

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

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

javaのソースをコピーし、kotlinのソースにペーストすると、AndroidStudioがkotlinに変換してくれる

地味ですがビックリしました。kotlin化推進に対する誰かの熱き想いを感じます。

static メソッドが使えず、シングルトン(object定義)にした

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

java
public class DIMock {
kotlin
object DIMock {

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

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

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

javadoc -> KDoc になった

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

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

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

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

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

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

java
    static {
kotlin
    init {

Class型の取得方法が変わる

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

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

javaでクラス型取得
        Class<DIMock> jclazz = DIMock.class;
        Class<DIMock> jclazz = DIMock.INSTANCE.getClass();
kotlinでクラス型取得
        val jclazz : Class<DIMock> = DIMock::class.java
        val jclazz : Class<DIMock> = DIMock.javaClass
        val kclazz : KClass<DIMock> = DIMock::class

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

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:匿名メソッド渡し
    fun <T> registDICallback(clazz: Class<T>, callback: (listener :T) -> Unit) {
    }
    DIMock.registDICallback(MainActivity::class.java) { activity :MainActivity ->}

kotlinの場合、javaの様にinterface定義しなくて済みます。
勿論、interface定義して匿名クラスを渡す事も出来ます。
単一メソッドのinterfaceは上記、複数のメソッドなら下記を用いるとコード量が減るかもですね。

kotlin:interface型の匿名クラス渡し
    DIMock.registDICallback(MainActivity::class.java, object : DIMock.DICallback<MainActivity> {
        override fun onInject(listener: MainActivity) {
        }
    })

kotlinの匿名クラス渡しは、javaのラムダ式より少し煩雑になりますね。。。

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

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

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: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:匿名メソッド渡しのパターン
    // 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
    boolean accessible = field.isAccessible();
    field.setAccessible(true);
kotlin
    val accessible = field.isAccessible
    field.isAccessible = true

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

java
sCustomMocks.put(clazz, mock);
kotlin
sCustomMocks[clazz] = mock

変換後のソースコード

変更前のソースは「こちら」

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のソースとして最適かと言われると、正直、自信が無いのが本音ではあります。
今後、継続的にレベルアップ&ブラッシュアップしたいと思っています。

いかがでしたでしょうか

何かのご参考になれば幸いです。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?