炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Swift の勢いで Kotlin 書いたら 3 行で 3 つのクラッシュを起こした事に対する反省文


2017 年アクトインディ アドベントカレンダー が始まりました。今年で 3 年目になります。1年目、2年目は技術寄りの投稿が多かったのですが、今年は、社員全員に公募を募り、様々なテーマで書いて行きますので、是非、今後の記事も楽しみにしていただければと思います。

2016 年アクトインディ アドベントカレンダー

2015 年アクトインディ アドベントカレンダー

1日目の記事を担当する namikata です。アプリエンジニアをしています。宜しくお願いします。今年最初の記念すべき記事は、反省文からのスタートです!自戒の念を込めてまとめて行きたいと思います。

iOS と Android の両方の開発に関わらせてもらっていますが、しばらく Swift で開発を行った後、Swift の余韻が残った状態で Kotlin の開発にシフトチェンジしたら、たった 3 行のコードで 3 種類のクラッシュ を発生させてしまう悪行を働いてしまいました。今回はそのコードを例にあげ Swift エンジニアが Kotlin で開発する際の心構えの基本的なところを伝えていければと思っています。

サンプルは、緯度経度の情報を元に都道府県を取得する以下のコードです。これには 3 つの重要なエッセンスが含まれています。

val geocoder = Geocoder(context)
val addressList = geocoder.getFromLocation(lat, lng, 1)
val adminArea = addressList.first().adminArea

Kotlin は Null 安全だけど Java が混じった Kotlin は Null 安全ではない

まず一つ目は getFromLocation メソッドです。 getFromLocation は引数に緯度、経度、maxResults を取り List<Address> を返却してくれるメソッドです。val addressList = geocoder.getFromLocation(lat, lng, 1) と型推論を利用して、型の記述を省略していますが addressList の型は何になっているでしょうか?

getFromLocation は List<Address> を返却してくれるので、もちろん List<Address> 型となっています。その為、以下のように書き換える事ができます。

val geocoder = Geocoder(context)
val addressList: List<Address> = geocoder.getFromLocation(lat, lng, 1)
val adminArea = addressList.first().adminArea

コンパイルエラーも起きません。しかし、実態は違います。 getFromLocation のコードを見ると、以下のように RemoteException エラーの際には null が返却されるのが分かります。

public List<Address> getFromLocation(double latitude, double longitude, int maxResults)
    ... 省略 ...
    try {
        ... 省略 ...
    } catch (RemoteException e) {
        Log.e(TAG, "getFromLocation: got RemoteException", e);
        return null;
    }
}

Java で書かれたコードを呼び出す際には、このような事が多くある為、対策としては、呼び出すメソッドを確認して null が返却されるかどうかをきちんと把握して書く必要があります。また、このようなケースで型推論を利用すると val addressList: List<Address> となる為 null が返却される場合には以下のように Nullable である事が分かるように書いてあげるのがいいかと最近感じています。

val geocoder = Geocoder(context)
val addressList: List<Address>? = geocoder.getFromLocation(lat, lng, 1)
val adminArea = addressList?.first().adminArea

val addressList: List<Address>? と書く事で addressList は Nullable である事がパッと見ても分かるようになり val adminArea = addressList.first().adminArea は addressList が Nullable の為、コンパイルエラーを出してくれるようになります。Swift の場合は、あまり意識せずにコードを書いていても、ほとんど NullPointer Exception に遭遇することはありませんでしたが、 Android では良く遭遇しました。

例外をスルーする first() メソッド

次の問題点は addressList.first() の部分です。

Swift では空のコレクションに対して first や last を呼び出すと nil が返ります。

[].first // nil

Kotlin の場合は空のコレクションに対して first() を呼び出すと例外が発生する為、クラッシュします。

listOf<Int>().first() // NoSuchElementException: List is empty

何一つ疑う事なく空になるコレクションに対して first() 使いまくってました。ごめんなさい。Kotlin では Swfit の first に相当する firstOrNull() があるので、空になる可能性のあるコレクションに対しては firstOrNull() を使いましょう。

listOf<Int>().firstOrNull() // null

先ほどのコードを改修すると以下のようになります。

val geocoder = Geocoder(context)
val addressList: List<Address>? = geocoder.getFromLocation(lat, lng, 1)
val adminArea = addressList?.firstOrNull()?.adminArea

リリースしてみないと中々発見できないクラッシュ

Geocoder#getFromLocation の実行で IOException: Service not available が発生したというクラッシュレポートが上がってきました。調べてみると Geocoder の getFromLocation は メモリ不足やアプリの強制終了などの理由により IOException: Service not available になることがあるそうです。

android - Service not available while calling geoCoder.getFromLocation() - Stack Overflow

対策はいくつかありますが、今回は例外が発生したらレポートを送信して、アプリがクラッシュしないように try catch するようにコードを変更してみました。

val geocoder = Geocoder(context)
val addressList: List<Address>? = try {
    geocoder.getFromLocation(latLng.latitude, latLng.longitude, 1)
} catch (e: IOException) {
    // 原因調査に必要な情報をレポートとして記録する
}
val adminArea = addressList?.firstOrNull()?.adminArea

Kotlin に限った話ではないですが、このような事例をリリースにテストで全て網羅するのは非常に難しいです。Crashlytics などのツールを利用して、クラッシュレポートを分析できるようにしておき、リリースに関しては、Google Play Console の段階リリースを利用して、クラッシュ発生の有無を確認しながら、徐々にリリース対象を広げていく方法で、被害を最小限に抑えるアプローチが必要になってくると思います。

まとめ

Swift エンジニアが初めて Kotlin でコードを書く際に気をつけるポイントを 3 つの事例でまとめてみました。

  1. Kotlin は Null 安全だから Android Studio のチェック機能に従って実装していれば NullPointerException は発生しないといった事はありません。
    • ライブラリのメソッドを参照する際は、メソッドの中身を確認し Null が返却されるかどうかを確認する事
    • Null が返却される場合には、変数に代入する際は型推論を避け、Nullable な型として宣言する事
  2. ドキュメントを読みましょう。当たり前の話ですが、言語が変われば仕様も異なるので、推測で書いてはダメだよといった話です。すいません、自分もドキュメント読み直します
  3. Google Play Console の段階リリースを利用して、クラッシュ発生の有無を確認しながら、リリースの対象を増やして行きましょう。特に Android 端末はメーカー各社がカスタマイズを施しているので、端末依存の不具合も少なくありません。リリースしてから発覚するクラッシュを想定して、段階リリースを使って行くのが良いかと思います。