Compose化した画面の初期化処理、initブロックでやるか?LaunchedEffect(Unit)内でやるか?
はじめに
my route開発部のAndroidエンジニア、Romie(@Romie_1112)です。
my routeのAndroidチームではUIの実装をxmlからJetpack Compose(以下Compose)へと粛々と切り替えております。
現在は地域別の特集コンテンツを並べた画面をCompose化しています。 希望の順番で並べ替えることもできます。
以下の順番で初回表示を行います。
1. 画面遷移する
2. 希望の順番を初期値:おすすめ順に設定する
3. リクエストの時に希望の順番をAPIに渡す
4. データを取得する
5. 取得したデータの一覧を表示する
実装する中で4. データを取得する処理について迷ったので、今回はそのお話をしたいと思います。
初期化の実装方法
これまでの実装は、希望の順番を渡してAPIを叩いた結果をLiveDataで通知し、observeで監視して値を取得してから画面を表示していました。
そのため、値を取得する前の初期化処理は実装されていませんでした。
しかし今回Compose化に伴いUiStateの値が変わればリアクティブプログラミングで即Fragmentに反映するStateFlowに変えることにし、LaunchedEffect(Unit)内で初期化するよう実装しました。
ここで初期化の実装にあたり、私は次に挙げる2つの方法で迷いました。
1. initブロックで初期化する場合
intiブロックで初期化する場合、以下のような実装になります。
data class FeatureSummaryListUiState(
val featureSummaryList: List<一覧のアイテム> = emptyList(),
)
private val _sortType = MutableStateFlow(おすすめ順)
private val _uiState = MutableStateFlow(FeatureSummaryListUiState())
val uiState = _uiState.asStateFlow()
init {
viewModelScope.launch {
_sortType.collectLatest { sortType ->
val summary = (APIを叩いてデータを取得)
_uiState.update {
it.copy(
featureSummaryList = (設定したい初期値),
)
}
}
}
}
setContent {
MyRouteTheme {
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
FeatureSummaryListScreen(
uiState = uiState,
)
}
}
initブロックについての記載を公式リファレンス[1]から見てみましょう。
The primary constructor initializes the class and sets its properties. In most cases, you can handle this with simple code.
If you need to perform more complex operations during instance creation, place that logic in initializer blocks inside the class body. These blocks run when the primary constructor executes.
Declare initializer blocks with the init keyword followed by curly braces {}. Write within the curly braces any code that you want to run during initialization:
initブロックは引用にもあります通り、インスタンスが形成された時に実行されるものになります。
インスタンスが形成された時に一度だけ呼ばれますので、初期化の処理を書くのにぴったりです。
ただし、initブロックはインスタンス形成時に呼ばれるという性質上、単体テストで初期化がちゃんとできているか見ることが厳しく、また単体テストの記載に慣れていないとinitブロックを考慮したテストを書くのが大変です。
2. LaunchedEffect(Unit)内で初期化する場合
では、FragmentからViewModel内の初期化処理をコールした場合はどうでしょうか。
最初に一度だけ呼ぶ処理だとコードを読む人に明示するためLaunchedEffect(Unit)の中に書くことをお勧めします。
data class FeatureSummaryListUiState(
val featureSummaryList: List<一覧のアイテム> = emptyList(),
)
private val _sortType = MutableStateFlow(おすすめ順)
private val _uiState = MutableStateFlow(FeatureSummaryListUiState())
val uiState = _uiState.asStateFlow()
fun initFeatureSummaryListUiState() { // initがfunになっています
viewModelScope.launch {
_sortType.collectLatest { sortType ->
val summary = (APIを叩いてデータを取得)
_uiState.update {
it.copy(
featureSummaryList = (設定したい初期値),
)
}
}
}
}
setContent {
MyRouteTheme {
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
LaunchedEffect(Unit) {
viewModel.initFeatureSummaryListUiState() // ここが違う!
}
FeatureSummaryListScreen(
uiState = uiState,
)
}
}
Composeにおける副作用[2]に副作用(LaunchedEffect)の説明がございます。
副作用とは、コンポーズ可能な関数の範囲外で発生するアプリの状態の変化を指します。
コンポーザブルのライフサイクルとプロパティ(予測できない再コンポジション、異なる順序でのコンポーザブルの再コンポジション、破棄可能な再コンポジションなど)により、コンポーザブルは副作用がないようにするのが理想的です。
ただし、スナックバーを表示するなどの1回限りのイベントをトリガーする場合や、特定の状態で別の画面に移動する場合などに、副作用が必要になることがあります。
これらのアクションは、コンポーザブルのライフサイクルを認識している制御された環境から呼び出す必要があります。
コールサイトのライフサイクルと一致する作用を作成するには、Unitやtrueのような決して変化しない定数をパラメータとして渡します。
この実装には次の一般的なメリットがあります。
- 再利用性:どの箇所からでも呼び出せる
- テスト容易性:独立した関数で実装しているため単体テストがやりやすい
また、プロジェクト内のメリットとして以下が挙げられます。
- 既存のコードとの整合性:他の箇所を確認したところCompose化した画面の初期化は
LaunchedEffect(Unit)内で行っていることが多く整合性が取りやすい
ただし、デメリットもあります。
- UDFの法則に反する:ViewModel→Fragmentという単方向でデータが流れる[4]べきなのにFragment→ViewModelとなってしまう[5]
- 依存度が高まり疎結合が崩れる:FragmentでViewModelの処理が呼ばれると依存度が高まりMVVMの目的の1つである疎結合が崩れる
- 呼び忘れる恐れがある:
LaunchedEffect(Unit)をはじめどこからでも呼び出せる代わりに呼び忘れる恐れがある
補足:発展編
今回の内容についてより高度な議論をJaewoong Eum氏がこちらの記事[6]にて行っております。
Androidコミュニティに対してアンケートを取得した上で、Ian Lake氏のツイートを引用してinitブロックもLaunchedEffect(Unit)内での初期化もアンチパターンであり
SharingStarted.WhileSubscribed(5_000)を活用した初期値の設定を紹介しています。
ただ、私は以下の懸念について検討した上で今回はSharingStarted.WhileSubscribed(5_000)を使用しませんでした。
一般的な点では
- 可読性の低下:複数のプロパティを持つUiStateを
SharingStarted.WhileSubscribed(5_000)で管理すると実装が複雑になり却って可読性が下がる
プロジェクト内の点では
- 既存のコードとの整合性の低下:
LaunchedEffect(Unit)内で初期化している画面が多いことから既存のコードとの整合性が取りづらくなる
です。
Jaewoong Eum氏の記事は今回ご紹介したものも含めて非常に勉強になりますので、全て英語ですが興味のある方は是非読んでみてください。
まとめ
今回はLaunchedEffect(Unit)内で初期化したのですが、initブロックで初期化する場合とLaunchedEffect(Unit)内で初期化する場合、2つのメリットとデメリットを比較した上で、以下の点を重視しました。
- テスト容易性:独立した関数で実装しているため単体テストがやりやすい
- 既存のコードとの整合性:他の箇所を確認したところCompose化した画面の初期化は
LaunchedEffect(Unit)内で行っていることが多く整合性が取りやすい
また、希望の順番を変えて並べ替えを行った時以下の順番で再表示を行います。
1. 並べ替えボタンを押下する
2. 希望の順番を任意の並べ替えに設定する
3. リクエストの時に希望の順番をAPIに渡す
4. データを取得する
5. 取得したデータの一覧を表示する
ここから4. データを取得する処理を1つの関数で実装し、初回表示時も希望の順番を変えて並べ替えを行った時も希望の順番をAPIに渡して関数を呼び出す形にした方がいいと考えました。
よって再利用性も重視しました。
- 再利用性:どの箇所からでも呼び出せる
理想を追求するといろんな方法が出てきますが、アンチパターンとされているものがあっても正解は1つではないですし、チーム内でレビューすること・後々の拡張性やテスト容易性を考慮しその都度1番良い実装を選択できると良いですね。
一番大切なのは、自分なりに理由や根拠を明確にして実装することです。
読んでいただきありがとうございました。それでは次の記事で。
出典元:Classes: Constructors and initializer blocks: Initializer blocksより一部抜粋 ↩︎
出典元:Composeにおける副作用より一部抜粋 ↩︎
出典元:rememberUpdatedState: 値が変化しても再起動すべきでない作用の値を参照するより一部抜粋 ↩︎
ViewModel内の値をFragmentが参照できない(ViewModelで何が起きているかFragmentが知らない)状態 ↩︎
FragmentがViewModel内で更新されている
featureSummaryListを参照できる状態 ↩︎出典元:Loading Initial Data in LaunchedEffect vs. ViewModelより一部抜粋 ↩︎
関連記事 | Related Posts

Figma MCPとClaude CodeでAndroidのUI構築を高速化

Jetpack Compose Animation Techniques: Enhance App Impressions with Minimal Code Changes

小規模なコード修正で効果的にアプリの印象を改善する、Jetpack Composeのアニメーション追加のテクニック

InlineContent in Jetpack Compose: The Hidden Gem for Complex Text UI

必須! 既存のAndroidアプリをedge-to-edge対応にするための速習ガイド

Android Composeオブジェクト指向ナビゲーション
