午前2時の呼び出し音
スマホが鳴ります。Crashlyticsのアラートが、最新リリースのクラッシュ率が15%急増していることを示しています。犯人は悪名高い UninitializedPropertyAccessException です。Kotlinコンパイラに対し、変数を使用する前に必ず初期化すると「指切りげんまん」をしたはずですが、Androidのライフサイクルには別の計画がありました。今、ユーザーは強制終了したアプリを見つめています。
FATAL EXCEPTION: main
Process: com.example.app, PID: 12345
kotlin.UninitializedPropertyAccessException: lateinit property viewModel has not been initialized
at com.example.app.ui.MainActivity.onCreate(MainActivity.kt:15)
クイックパッチ
クラッシュを防ぐ最も手っ取り早い方法は、プロパティに触れる前に初期化状態を確認することです。::prop.isInitialized 構文を使用します。これはコードの安全ゲートとして機能します。
if (::viewModel.isInitialized) {
viewModel.doSomething()
}
これをいたるところで多用しないでください。もし10箇所も isInitialized をチェックしているなら、おそらくアーキテクチャに構造的な欠陥があります。
なぜAndroidでこれが発生するのか
Androidフレームワークは、オブジェクトの生成とセットアップが分離されていることで有名です。ActivityやFragmentのコンストラクタを私たちが呼び出すことはありません。OSが呼び出します。そのため、依存関係をセットアップするには特定のライフサイクルコールバックを待つ必要があります。失敗しやすいポイントは以下の通りです:
- **インジェクションの遅延:** DaggerやHiltを使用している際、`super.onCreate()` を呼び出す前に `@Inject` フィールドに誤ってアクセスしてしまった。
- **Fragment Viewの罠:** `onDestroyView()` の後にFragmentのView Bindingオブジェクトにアクセスした。これはメモリリークとクラッシュの一般的な原因です。
- **競合状態(レースコンディション):** ネットワークリクエストが200msで返ってきた際、まだ `onCreate` シーケンスが完了していないUIコンポーネントを更新しようとした。
より優れた実装戦略
1. 適切なライフサイクルフロー
Activityの場合、すべての代入を onCreate() に移動します。Fragmentの場合、UIロジックには onViewCreated() が最も安全な場所です。バックグラウンドスレッドがある場合は、UIが受信準備を整えるまでコールバックが実行されないようにしてください。
// 危険:このコールバックはアダプターが生成される前に完了する可能性があります
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
api.fetchData { data ->
adapter.submitList(data) // apiのレスポンスが次の一行より速いとクラッシュします
}
adapter = MainAdapter()
}
2. Null許容パターン(より安全)
クラスの全生存期間にわたってプロパティが存在することが保証されない場合は、lateinit を使用せず、Null許容型(nullable type)を使用してください。これにより「null」状態の処理が強制され、致命的な例外よりもはるかに安全になります。
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null // クラッシュやリークを避けるためにクリーンアップする
}
3. 'by lazy' を活用する
初期化時に外部入力を必要としない読み取り専用プロパティには、by lazy を使用します。プロパティは呼び出されるその瞬間まで作成されません。スレッドセーフで効率的です。
private val analytics by lazy {
AnalyticsProvider.get(this)
}
修正を確認する方法
アプリが開いたからといって、クラッシュがなくなったと思い込まないでください。以下の手順に従ってください:
- **ストレステスト:** 開発者オプションの「アクティビティを保持しない」を有効にし、画面を素早く回転させます。これにより、ライフサイクルの破棄と再構築が強制されます。
- **Logcatの確認:** スタックトレースの `Caused by` の行を探します。画面間を素早く移動したときに、新しい `lateinit` エラーが表示されないことを確認してください。
- **ユニットテスト:** `mockk` を使用して、セットアップメソッドが実際に呼び出されているか確認します。Robolectricを使用している場合は、`activityController.create()` の後にプロパティが準備完了していることをアサートします。
最終結論
lateinit は利便性のためのツールであり、Null安全を無視するための手段ではありません。プロパティが本当にオプションであったり、動的に変化したりする場合は、Null許容型を使用してください。lateinit は、インジェクトされたViewModelや onCreate でセットアップされる特別なツールなど、確実に存在することが保証されている依存関係のために取っておきましょう。

