カテゴリー: Kotlin

KotlinScriptingを動かしてみる

kotlinでスクリプトファイルを作成して実行できるようにします。

https://kotlinlang.org/docs/custom-script-deps-tutorial.html

macではbrew install kotlinでkotlinをインストールします。

同一ディレクトリ内にスクリプトファイルを書きます。

exclude.kts

fun helloExclude() {
    println("hello exclude")
}

hello.main.kts

@file:Import("./exclude.kts")

println("hello main")
helloExclude()

実行したいファイルの末尾は「.main.kts」にします。ファイル名をこのようにすることで@file:Import()等が動くようになります。

実行するには下記のコマンドを実行します。

$ kotlinc -script hello.main.kts
hello main
hello exclude

AndroidStudioとVisualStudioCodeで試してみたのですが、main.kts側のコード自動補完が効かないのが辛いです。なんかやり方が間違っているのかなぁ。


[Android] シングルモジュールのプロジェクトでDetektによる静的解析をする

巷には多くの静的解析ツールがあります。無料でAndroidのプロジェクトを解析したい場合はDetektがおすすめです。

https://github.com/detekt/detekt

導入も簡単でした。今回はシングルモジュールの場合の導入方法を記載します。appモジュールのbuild.gradle.ktsに下記を追加します。

plugins {
  ....
   id("io.gitlab.arturbosch.detekt").version("1.23.5")
}

detekt {
    // 並列処理
    parallel = true

    // 違反があった場合failさせない
    ignoreFailures = true

    // レポートファイルのバスを相対パスにする
    basePath = rootDir.absolutePath
}

ここで一旦sync project with gradle filesを実行します。すると./gradlew detektが実行できるようになっているのでターミナルで実行します。Detektの結果はモジュール内のbuild/reports/detekt内に出力されます。detekt.html, detekt.md, detekt.sarif, detekt.txt, detekt.xmlがあるのであとは煮るなり焼くなり…


[Kotlin] Parameterizedテストを書く

ユニットテストを充実させていくと小さい関数のテストが必要になってきます。例えば下記のような拡張関数がある場合

fun Int.toBoolean(): Boolean = this == 1

Int型の変数が1の場合のみtrueでそれ以外はfalse。ParameterizedテストはJunitの機能で記述できます。

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class IntTest(
    private val cate: String,
    private val input: Int,
    private val expected: Boolean
) {
    @Test
    fun intToBoolean() {
        val actual = input.toBoolean()
        assertThat(actual).isEqualTo(expected) // junitのみで書きたい場合はassertEquals()でも良い
    }

    companion object {
        @Parameterized.Parameters(name = "{0}")
        @JvmStatic
        fun data() = listOf(
            arrayOf(
                "0の場合はfalse",
                0,
                false
            ),
            arrayOf(
                "1の場合はtrue",
                1,
                true
            ),
            arrayOf(
                "2の場合はfalse",
                2,
                false
            )
        )
    }
}

private val case: Stringはテストの内容を記述します。@Test内では利用されていませんが、実行結果やエラー時に表示されるので書いておくことをおすすめします。

caseを書かない場合

Parameterizedテストを書けるようにしておくとAndroidのコードから切り出したif文が正しく動作するか、data class内の関数が動作するかが確認できるようになります。


mockkで直接モックできない場合の対応方法

DBに時刻を保存したりログを送信したりするときに現在時刻をSystem.currentTimeMillis()を実行して現在時刻を取得します。mockkでモックする場合はスタティックモックで実行しようとすると正しくモックしてくれません。

fun returnTime() {
  mockkStatic(System::class)
  every { System.currentTimeMillis() } returns 1L
  assertEquals(System.currentTimeMillis(), 1L) // StackOverflowErrorが発生し失敗する
}

System.currentTimeMillis()はシステムのネイティブメソッドなのでモックすることができません。このような場合はobjectでラップしてモックできるようにします。

object SystemTime {
  fun current() = System.currentTimeMillis()
}

fun returnTime() {
  mockkObject(SystemTime)
  every { SystemTime.current() } returns 1L
  assertEquals(1L, SystemTime.current())
}

とにかくobjectでラップしてしまえば他の場合でも使えそうですね。

https://github.com/mockk/mockk/issues/98


Androidアプリのテストカバレッジを計測する

https://github.com/Kotlin/kotlinx-kover

jetbrain製のkotlin用テストカバレッジ計測ライブラリがありバージョン0.5前後のときは導入が難しかったのですが、0.7あたりから導入が簡単になりました。

Androidアプリのテストカバレッジは以前はjacocoなどが使われていましたが導入が難しかったり、java用のライブラリであったことから適切なコードカバレッジを測ってくれなかったり、Androidのマルチモジュールに対応していなくてレポートをマージする必要があったりと色々と面倒でした。

まずはカバレッジ計測したいモジュールのbuild.gradle.ktsに下記を追加します。マルチモジュールでもシングルモジュールでも同じです。

plugins {
    id("org.jetbrains.kotlinx.kover:0.7.1") // 2023年8月時点での最新版は0.7.3です。自分は0.7.1までの動作を確認
}

appモジュールのbuild.gradle.ktsに除外設定を追加します。

koverReport {
    filters {
        excludes {
            packages(
                "dagger*",
                "hilt*"
            )
            classes(
                "*Module_Provide*",
                "*Binding",
                "*_Factory",
                "*_HiltModules*",
                "*Hilt_*",
                "*BuildConfig",
                "*HiltWrapper_*",
                "*ComposableSingletons*",
                "*_MembersInjector*"
            )
        }
    }
}

除外設定は後述するHTMLレポートを出力しつつプロジェクトにあった設定を行ってください。サブクラスを除外したい場合は除外設定の末尾に*が必要です。

さらにappモジュールのbuild.gradle.ktsにカバレッジ計測したいモジュールを定義(追加)します。appモジュール自体の定義は不要です。

dependencies {
    kover(project(":moduleA")
    kover(project(":moduleB")
}

設定はほぼ終わりです。簡単ですね。sync project with gradle filesを実行後にターミナルから./gradlew tasks | grep koverを実行するとkoverのGradleタスクを確認できます。BuildVariantの設定により異なりますが./gradlew :app:koverHtmlReportDebugを実行するとプロジェクトルート/app/build/reports/kover/htmlDebug/index.htmlのHTMLレポートが出力されます。./gradlew koverXmlReportDebugを実行するとプロジェクトルート/app/build/reports/kover/reportDebug.xmlにXMLレポートが出力されます。ファイル名もタスク名と同様にBuildVariantによって異なります。

このHTMLをブラウザなどで確認してカバレッジに含めたくないファイルを除外設定に追加していくと綺麗なレポートが出来上がると思います。XMLレポートをcodecovと連携して時系列ごとのカバレッジの変化やGithubのにカバレッジバッジを表示できるのですが、それはまた別の機会に紹介します。


リフレクションを使うのはやめておいたほうが良さそう

https://kotlinlang.org/docs/reflection.html

kotlinにリフレクションという機能があり、privateなプロパティを書き換えたりすることが可能です。Androidだと本来設定できないGUIの表示を書き換えたりできます。しかしこの機能は本来アクセスすることができないパラメータを使うのでライブラリのアップデートや難読化の影響で動作しなくなる場合があるので使わないほうが良さそうです。(有用な使い方も今のところわからん)


[Kotlin]interfaceをインスタンス化する

このサイトのアクセス解析を見ると意外と簡単な内容というかシンプルな内容のアクセスが多いので今回もサラッと終わる内容になります。

Kotlinではinterfaceを継承したインスタンスを簡単に作ることが可能です。下記のようなinterfaceがあるとします。

interface HasName {
    val name: String
}

class Human : HasNameというクラスを作成しても良いですが、クリックリスナーとか「継承したクラスを作成するほどではないな」という時は下記のように書くことでinterfaceを継承したインスタンスを作成できます。

fun main() {
    val human = object : HasName { override val name = "suzuki" }
    println(human.name)
}

https://pl.kotl.in/4T0rVkYhM


[Kotlin]関数の戻り値の型を書かないと発生するバグ

下記のようなコードがあるとします。

interface HasAnalogStick {
    fun lowerStick()
}
interface GameConsole

object WiiU : GameConsole, HasAnalogStick {
    override fun lowerStick() { /* スティックを倒す */ }
}
object NintendoSwitch : GameConsole, HasAnalogStick {
    override fun lowerStick() { /* スティックを倒す */ }
}

fun randomGameConsole() = listOf(WiiU, NintendoSwitch).random()

fun main() {
    val console = randomGameConsole()
    println(console.toString() + ": lower stick")
    (console as HasAnalogStick).lowerStick()
}

https://pl.kotl.in/bqS8bIsil

このコードはコンパイルが通り、実行可能ですし、fun randomGameConsole()でWiiUが返って来てもNintendoSwitchが返ってきてもメイン関数内の処理も全て実行されます。

のちにリファクタが必要になりobject Famicomとobject SuperFamicomが追加されたとします。

interface HasAnalogStick {
    fun lowerStick()
}
interface GameConsole

object Famicom : GameConsole
object SuperFamicom : GameConsole
object WiiU : GameConsole, HasAnalogStick {
    override fun lowerStick() { /* スティックを倒す */ }
}
object NintendoSwitch : GameConsole, HasAnalogStick {
    override fun lowerStick() { /* スティックを倒す */ }
}

fun randomGameConsole() = listOf(Famicom, SuperFamicom, WiiU, NintendoSwitch).random()

fun main() {
    val console = randomGameConsole()
    println(console.toString() + ": lower stick")
    (console as HasAnalogStick).lowerStick()
}

https://pl.kotl.in/7sJJg3HCT

このコードだとどうでしょうか?コンパイルが通るので一見良さそうに見えますが、実行時にfun randomGameConsole()でFamicomまたはSuperFamicomが返されるとそれらはHasAnalogStickを継承していないので、実行時エラーになります。実行しないとわからないエラーがあるのは良くないコードです。このような実行しないとわからないエラーを無くすにはfun randomGameConsole()の戻り値の型を書くようにしましょう。fun randomGameConsole(): HasAnalogStickと宣言すればコンパイル時にエラーとなるのでコードを実行しなくてもエラーがあることがわかります。

Kotlinでは関数の戻り値の型は省略することは可能ですが、特に理由が無いのであれば戻り値の型を書きましょう。


runCatchingとResultを使って例外処理を簡単に書く

Exceptionを投げる関数を実装するときはどのように実装しますか?通常は下記のように書くことが多いのではないでしょうか?

// 成功の場合はtrueを返す、失敗の場合はfalseを返す
fun randomFunction(): Boolean {
  var outcome = false
  try {
    val response = useCase.execute()
    _liveData.postValue(response)
    outcome = true
  } catch (e: Exception) {
    outcome = false
  }
  return outcome
}

try-catchするのも良いのですが、ネストも深くなるしコードが突然catch内に飛ぶし、var変数使わないといけない場面があったりして不便ですし個人的にも嫌いですw。そういうときはrunCatchingを使います。

fun randomFunction(): Boolean {
  val result = useCase.execute()
  if (result.isSuccess) _liveData.postValue(result.getOrThrow())
  return result.isSuccess
}

runCatchingを使うとネストも深くならないし、コードも飛ばないしvarを使う場面も減らせます。runCatchingの結果はResult型で返ってきてResult::isSuccess、Result::isFailure、Result::onSuccess()、Result::onFailure{}、、Result::getOrThrow()、Result::getOrNull()、Result::exceptionOrNull()等の便利な関数も用意されております。try-catchはやめてrunCatchingを使いましょう。


MoshiでJsonデシリアライズするとCould not compute caller for function: public constructor クラス名と表示されてクラッシュする

APIからJsonを取得しデシリアライズするときは以前はGsonが使われていましたが最近はMoshiが使われています。アプリの本番ビルドをしたときは難読化の影響を受けるのでMoshiでデシリアライズする場合はそれを考慮に入れないとクラッシュしてしまいます。その場合はproguard-rules.proに下記のようにルールを追加します。

-keepclassmembers class ファイルが存在するパッケージ名.** { *; } // **にデシリアライズするときのモデルのファイルがある

proguard-rules.proの書き方や他のオプションもあるのですが、とりあえずクラッシュだけさせない場合は上記のみ書けばクラッシュしなくなります。