たくさんの自由帳

AndroidのNonNullアノテーションはnullを返す場合があり、Kotlinで問題になる話

投稿日 : | 0 日前

文字数(だいたい) : 8203

Android 「この値は @NonNull やで」→ Kotlin 「ほな厳密なnullチェックするで」→ Android 「実行時にやっぱ null 返すわ!」→ NullPointerException

うそつくな

本題

Android@NonNullアノテーションはたまによくnullを渡す。これがJavaなら問題なかったけど、Kotlinだと例外で落ちてしまうって話。

おことわり

本記事で言う@NonNullandroid.annotationandroidx.annotationのことです。

Imgur

修正する

多分2パターン存在します。

Javaファイルを作成し、@NonNullなインターフェースを継承し、@NonNullを全部@Nullableにする

.javaを作る必要があるので、なんか負けた気分になります(が、Android@NonNull守らないのが悪いので仕方ない、、、)

例えば、こんな感じにJavaで書かれたインターフェースに@NonNullがついている場合
(以下はAndroidOnGestureListenerです)

public interface OnGestureListener {
    boolean onDown(@NonNull MotionEvent e);

    void onShowPress(@NonNull MotionEvent e);

    boolean onSingleTapUp(@NonNull MotionEvent e);

    boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY);

    void onLongPress(@NonNull MotionEvent e);

    boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY);
}

自前でこのインターフェースを継承するインターフェースを作成し、@NonNull@Nullableに置き換えます。
このときJavaで作成する必要があります。

import android.view.GestureDetector;
import android.view.MotionEvent;

import androidx.annotation.Nullable;

/** OnGestureListener を Nullable にしたもの */
public interface NullableOnGestureListener extends GestureDetector.OnGestureListener {

    @Override
    boolean onDown(@Nullable MotionEvent e);

    @Override
    void onShowPress(@Nullable MotionEvent e);

    @Override
    boolean onSingleTapUp(@Nullable MotionEvent e);

    @Override
    boolean onScroll(@Nullable MotionEvent e1, @Nullable MotionEvent e2, float distanceX, float distanceY);

    @Override
    void onLongPress(@Nullable MotionEvent e);

    @Override
    boolean onFling(@Nullable MotionEvent e1, @Nullable MotionEvent e2, float velocityX, float velocityY);
}

これで、KotlinでもNullableとして扱ってくれるので、nullが入ってきた場合でも落ちなくなります。
MotionEventが全部Nullableになりました。やったね

class MainActivity : ComponentActivity(), NullableOnGestureListener {

    override fun onDown(e: MotionEvent?): Boolean {
        // TODO
    }

    override fun onShowPress(e: MotionEvent?) {
        // TODO
    }

    override fun onSingleTapUp(e: MotionEvent?): Boolean {
        // TODO
    }

    override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
        // TODO
    }

    override fun onLongPress(e: MotionEvent?) {
        // TODO
    }

    override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
        // TODO
    }

    // 省略...
}

もう一つ

これはやっていいのかわからないのですが、どうしてもKotlinで完結させたい場合は使えます。
Suppressで黙らせるやつですね。引数がNonNullからNullableになるだけ(引数が増えているわけではない)なので、おそらく実行時に落ちることはないと思いますが、、、

@Suppress("NOTHING_TO_OVERRIDE", "ACCIDENTAL_OVERRIDE", "ABSTRACT_MEMBER_NOT_IMPLEMENTED")
class MainActivity : ComponentActivity(), GestureDetector.OnGestureListener {

    override fun onDown(e: MotionEvent?): Boolean {
        // TODO
    }

    override fun onShowPress(e: MotionEvent?) {
        // TODO
    }

    override fun onSingleTapUp(e: MotionEvent?): Boolean {
        // TODO
    }

    override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
        // TODO
    }

    override fun onLongPress(e: MotionEvent?) {
        // TODO
    }

    override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
        // TODO
    }

    // 省略....
}

事の発端

java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter cellInfo
	at io.github.takusan23.newradiosupporter.tool.NetworkCallbackTool$listenNetworkStatus$1$callback$1.onCellInfoChanged(Unknown Source:2)
	at android.telephony.TelephonyCallback$IPhoneStateListenerStub.lambda$onCellInfoChanged$18(TelephonyCallback.java:1504)
	at android.telephony.TelephonyCallback$IPhoneStateListenerStub$$ExternalSyntheticLambda41.run(Unknown Source:4)
	at android.os.Handler.handleCallback(Handler.java:938)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loopOnce(Looper.java:346)
	at android.os.Looper.loop(Looper.java:475)
	at android.app.ActivityThread.main(ActivityThread.java:7889)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1009)

onCellInfoChangedNullPointerExceptionになってしまう。
原因はonCellInfoChangedAndroidが呼び出す際にcellInfonullで渡しているため、、、

/**
 * Interface for cell info listener.
 */
public interface CellInfoListener {
    /**
     * Callback invoked when a observed cell info has changed or new cells have been added
     * or removed on the registered subscription.
     * Note, the registration subscription ID s from {@link TelephonyManager} object
     * which registers TelephonyCallback by
     * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}.
     * If this TelephonyManager object was created with
     * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the
     * subscription ID. Otherwise, this callback applies to
     * {@link SubscriptionManager#getDefaultSubscriptionId()}.
     *
     * @param cellInfo is the list of currently visible cells.
     */
    @RequiresPermission(allOf = {
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.ACCESS_FINE_LOCATION
    })
    void onCellInfoChanged(@NonNull List<CellInfo> cellInfo);
}

Androidの@NonNullアノテーション

これ単純にビルド時nullの可能性があるときに警告を出すものなので、ライブラリ開発者とかは意識するといいかもしれないですが、それ以外ならぶっちゃけ無くてもまあ、、、

public class NonNullCheck {

    NonNullCheck() {
        methodA("はい");
        // 警告が出るだけでビルドは通ってしまう
        methodA(null);
    }

    private void methodA(@NonNull String argA) {
        // NonNull でも null 入るときは入るので
        if (argA == null) {
            return;
        }
        System.out.println(argA);
    }
}

Imgur

一方 Kotlin の NonNull

KotlinNullを厳格にチェックするため、?がついていない引数はビルドも通らないし、実行時も落ちるようになっています。

class NonNullCheckKt {

    init {
        methodA("はい")
        // ビルドは通らない
        methodA(null)
    }

    fun methodA(argA: String) {
        // argA は null にはならない
    }

}

また実行時にもNonNullは機能し、関数内に引数がnullではないことを確認する処理が自動で挿入されています。
(全然Androidとは関係ないKotlinプロジェクトだけど変わらないはず)

/**
 * ドロップ数を返す。失敗したら1
 *
 * @param itemStack アイテム
 * */
private fun getDropSize(itemStack: ItemStack): Int {
    return itemStack.name.string.toIntOrNull() ?: 1
}

逆コンパイルするとnullチェックする関数が挿入されている

private final int getDropSize(class_1799 itemStack) {
  Intrinsics.checkNotNullExpressionValue(itemStack.method_7964().getString(), "itemStack.name.string");
  StringsKt.toIntOrNull(itemStack.method_7964().getString());
  return (StringsKt.toIntOrNull(itemStack.method_7964().getString()) != null) ? StringsKt.toIntOrNull(itemStack.method_7964().getString()).intValue() : 1;
}

Javaで書いた@NonNullはKotlinだと?

さて、@NonNullでも実行時はnullを渡す可能性がある話をしましたが、、、いかが。

まぁ予想通りKotlinでもアノテーションを尊重してNonNullとして扱われます。
https://kotlinlang.org/docs/java-interop.html#nullability-annotations

今回、@NonNullJavaの場合に落ちないけど、Kotlinの場合にいきなり落ちるようになったのはこの影響です。

public interface NonNullInterface {
    void onCallback(@NonNull String string);
}
class NonNullCheckKt : NonNullInterface {
    override fun onCallback(string: String) {

    }
}

以上です。

おわりに

targetSdk = 33から、MotionEventNonNullになった影響でわりとIssueがちらほら(全然良くない;;)

ちなみに私が引っかかったonCellInfoChangedに関してもnullを返さないよう修正されたそうですが、古いバージョンには残り続けるでしょうね、、、
https://issuetracker.google.com/issues/237308373