月: 2022年7月

[Android]Intentのユニットテスト実行時に Caused by: java.lang.IllegalAccessExceptionが発生し、テストが失敗する場合の対応

Gradleを7.0.2に、com.android.tools.build:gradleのバージョンを7.0.4に変更したところアプリビルドと実行はできますが、Intentを使ったテストが失敗するようになりました。androidx.test.espresso:espresso-intentsとandroidx.test.espresso:espresso-contribのバージョンを3.5.0以上(2022年7月現在3.5.0-alpha01以上)にするとテストできます。

https://mvnrepository.com/artifact/androidx.test.espresso/espresso-intents

[Android]Roomを含むテスト実行時にcom.almworks.sqlite4java.SQLiteExceptionが発生し、テストが実行できない場合の対応

開発マシンをIntelChipからAppleM1に変更しました。さらにGradleを7.0.2に、com.android.tools.build:gradleのバージョンを7.0.4に変更したところアプリビルドと実行はできますが、Roomのマイグレーションのテストが失敗するようになりました。マイグレーションのテストがRobolectricを使用しており、RobolectricがM1に対応した4.7以上でないとテストが通らないので4.7以上にアップデートしましょう。

https://github.com/robolectric/robolectric/issues/6311

https://mvnrepository.com/artifact/org.robolectric/robolectric


[Android]Roomを使っているプロジェクトでjava.lang.reflect.InvocationTargetException (no error message)と表示されアプリのビルドができない場合の対応

開発マシンをIntelChipからAppleM1Proに変更しました。ソースコードをクローンしてビルドしたところ下記のエラーが表示されビルドできませんでした。

Execution failed for task '<モジュール名>:kaptDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction
   > java.lang.reflect.InvocationTargetException (no error message)

このモジュール名の部分がRoomでDB読み書きを行っているモジュールだったので、Roomのバージョンを上げたところビルドと実行ができるようになりました。具体的にはandroidx.room:room-runtime:[VERSION]、androidx.room:room-ktx:[VERSION]、androidx.room:room-compiler:[VERSION]、androidx.room:room-testing:[VERSION]のバージョンを2.2.6から2.4.0以上に上げました。2.3.0もビルドできないので2.4.0以上に上げましょう。


Architectural Decision Recordsを書いておく

https://adr.github.io

複数人で開発することがモバイルアプリケーションでも増えてきています。コードを実装する上で「ここはどういうルールで実装するべきなのか?」「他の部分では古い設計だけど、新しい設計にしたほうが良いからしてしまおう」など複数人で開発しているとどのようにコードを書くべきか迷う場面が出てきます。そのとき参照すべきドキュメントやルールはチーム内で揃っていますか?現状の設計が古いとしても開発初期段階ではその設計が最善だったかもしれませんし、新しい設計を入れるにしてもチームで意思統一ができていないと設計もバラバラになり、修正やリファクタリングも難しくなります。そういうときのためにArchitectural Decision Recordsを書いておくと後で迷う場面が出てきても「なぜこういう設計になっているんだっけ?」という迷いが生じなくなります。フォーマットは色々ありますが、下記のような内容を書いておくのが良いです。

https://qiita.com/fuubit/items/dbb22435202acbe48849

  • タイトル(Title)
  • ステータス(Status)
  • 意思決定者
  • コンテキスト(Context)
  • 決定(Decision)
  • 結果(Consequences)

自分も「テストコードを書くためにこういう設計にしたい」と業務で思ったときはADRを書いてミーティングを開く手順を踏むことにしました。全員の意志が同じ方向に向かっているとレビューや会話もスムーズになりますし、新しいメンバーが入ったときもルールを統一して開発に挑めます。


operator fun invoke()の使い方

kotlinでは()を使ってインスタンスを作成します。例えばMyClassという名前のクラスをインスタンスにする場合は下記のように記述します。

val myInstance = MyClass()

しかし初期化方法が複雑化してきたりすると下記のようにstaticな関数を用意することが多いと思います。

class MyClass {
    lateinit var str: String 
    companion object {
        fun createInstance(str: String): MyClass {
            val myInstance = MyClass()
            myInstance.str = str
            return myInstance
        }
    }
}
fun main() {
    val myInstance = MyClass.createInstance("aiueo")
    print(myInstance.str)
}

https://pl.kotl.in/SSo3QMwLe

これはこれで良いのですが、MyClassを使う側の人はMyClassにcreateInstanceという関数が存在してそれを使って初期化をするというMyClassの実装を意識しないといけません。MyClassを使う側の人にとっては読むコードの量が多くなるのであまり良いことではありません。こういう時にoperator fun invoke()を利用します。

class MyClass {
    lateinit var str: String 
    companion object {
        operator fun invoke(str: String): MyClass {
            val myInstance = MyClass()
            myInstance.str = str
            return myInstance
        }
    }
}
fun main() {
    val myInstance = MyClass("aiueo")
    print(myInstance.str)
}

https://pl.kotl.in/l3-R7NU9n

このように書くとcreateIntanceと同じ機能を実装可能です。多くの高級言語ではクラス名()でクラスをインスタンス化するので、MyClassのcreateInstanceが生えているという実装の中身を知らなくてもMyClass()でインスタンス化できます。引数はAndroid StudioやIDEを使っていると保管されて引数にstr: Stringが必要なことが表示されるため、実装を間違える可能性は低いです。

operator fun invoke()の使いみちは色々ありますが、Fragmentの生成やViewHolderの生成等にも利用可能です。Fragmentの例を下記に置いておきます。

MyClassclass MyFragment : Fragment() {
    companion object {
        const val queryKey = "query"
        const val isSystemApplicationKey = "is_system_application"
        operator fun invoke(param: Param?): MyFragment =
                MyFragment().apply {
                    val bundle = Bundle().also { b ->
                        param?.let { p ->
                            b.apply {
                                putString(queryKey, p.query)
                                putBoolean(isSystemApplicationKey, p.isSystemApplication)
                            }
                        }
                    }
                    this.arguments = bundle
                }
    }
}

operator fun invoke()という見た目は一瞬意味がわからないのですが、()を置き換える役目なので使ってみてはいかがでしょうか。