0️⃣

value class をデフォルト引数にするとNullPointerExceptionが投げられる場合がある

2023/08/22に公開

概要と結論

Jetpack Composeでvalue classをデフォルト引数ありで引数にした場合、引数指定なし(デフォルト引数を利用)で呼び出すとNullPointerExceptionが投げられます。

解決法

value class でないようにすればよいです。(当たり前といえば当たり前)

  • typealiasを利用する。
  • data classにする。
  • 等々

詳しく

経緯

DroidKaigi2023が今年も行われます。
contribute大歓迎ということで、私も何かしてみようと思い、挙がっていたissueを解決するPull Requestを作りました。

https://github.com/DroidKaigi/conference-app-2023/pull/756/

その際に拡張関数を用意したかったのでvalue classを使いました。
さくっと作って、PullRequestを送ったところPullRequestのUnitTestで失敗していることがわかったので調査しました。

調査

調査といってもこのPullRequestでのコードの変更点は少なかったので予想はついていました。

以下のコードをビルドしてみます。

MainActivity.kt
@JvmInline
value class Message(val text: String) {
    fun asQuote() = "「$this」です。"
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ValueClassTestTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting()
                }
            }
        }
    }
}

@Composable
fun Greeting(name: Message = Message("Hello Android"), modifier: Modifier = Modifier) {
    Text(
        text = name.asQuote(),
        modifier = modifier
    )
}

問題なくビルドできますが、アプリが起動すると直ぐに、NullPointerExceptionが投げられて、以下のようなログが記録されると思います。

java.lang.NullPointerException: Parameter specified as non-null is null: method com.s_h_y_a.valueClassTest.MainActivityKt.Greeting-lFd755E, parameter name
	at com.s_h_y_a.valueClassTest.MainActivityKt.Greeting-lFd755E(Unknown Source:8)
	at com.s_h_y_a.valueClassTest.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:30)
	at com.s_h_y_a.valueClassTest.ComposableSingletons$MainActivityKt$lambda-
1$1.invoke(MainActivity.kt:29)
   ...(略)

ログにあるMainActivity.kt:30は以下のGreeting()を呼び出す部分です。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ValueClassTestTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting() // ←ここ
                }
            }
        }
    }
}

まさにvalue classがデフォルト引数に設定されている関数を引数無しで呼び出していると思います。ちなみに引数を指定した場合は例外は投げられません。
value classはコンパイルする際に、inline関数のように展開された状態としてビルドされるので、それがKotlinのデフォルト引数と相性が悪いのだと思われます。

厄介なところ

  • コンパイルエラーが起こらない
    • 文法上は問題ないので実行時にならないと発覚しません。
  • 発生条件が緩い
    • 今回は起動直後にクラッシュしましたが、特殊な画面の実装にて使用してエラーが起こることも考えられます。
  • Previewでは正常に表示される
    • これが本当に厄介。そのため気づきにくいです。

最後に

初記事のためつたないところもあるかもしれませんがご了承ください。
この問題を再現したリポジトリはこちらとなります。
引数を"いんすう"と呼んでしまう変な癖のせいでこの記事を書く際に苦労しました。

Qiita:https://qiita.com/S-H-Y-A/items/f47b8ceff9df20da506b

参考

value class

https://kotlinlang.org/docs/inline-classes.html

デフォルト引数

https://kotlinlang.org/docs/functions.html#default-arguments

Discussion