StateFlow — Kotlin Coroutinesライブラリのリアクティブステートコンテナであり、StateFlow<T>を表します — Flowのサブタイプで、常に現在の値を保存し、新しいサブスクライバーに発行します。StateFlowの本質を説明します:LiveDataとは異なり、StateFlowはAndroidフレームワークに依存せず、任意のKotlinプラットフォームで動作します。Google(Android Developers、2025)によると、StateFlowは新しいKotlinプロジェクト、特にJetpack Composeを使用したMVVMアーキテクチャにおいて、LiveDataの主要な代替として推奨されています。
重要なポイント
collectAsState()、ViewでrepeatOnLifecycle()を使用します。StateFlowはkotlinx.coroutines.flowライブラリのインターフェースで、replayパラメータを1に固定したMutableSharedFlowを拡張します。つまり、StateFlowは常に最後に送信された値を記憶し、新しいサブスクライバーに即座にリプレイします。LiveDataとは異なり、StateFlowは標準のKotlin Coroutinesライブラリの一部であり、Androidへの依存関係はありません。
概念的には、StateFlowはリアクティブプロパティです。.valueで現在の値を読み取り、.collect()で変更をサブスクライブします。このモデルは「ホットフロー」と呼ばれ、データソースはサブスクライバーの有無にかかわらずアクティブです。flow { }で作成される「コールド」フローはサブスクライバーが現れたときに開始されます。
StateFlowはkotlinx.coroutines 1.3.7(2020年12月)で安定化され、Google I/O 2021以降、GoogleによってLiveDataの代替として推奨されました。JetBrainsの調査によると、2025年1月時点で、Kotlinの新しいAndroidプロジェクトの56%がStateFlowを主要なリアクティブコンテナとして使用しています。
StateFlowとLiveDataの選択は、プロジェクトのアーキテクチャ、技術スタック、プラットフォーム非依存性の要件によって異なります。以下は6つの主要な基準による比較です。
| 基準 | StateFlow | LiveData |
|---|---|---|
| プラットフォーム | Kotlin Multiplatform(Android、iOS、サーバー) | Androidのみ |
| Lifecycle対応 | いいえ — repeatOnLifecycle()が必要 | はい — 組み込みバインディング |
| コルーチン | 完全サポート(map、filter、combine) | liveData { }ビルダー経由 |
| Null安全性 | はい — kotlinx.serializationでシリアル化可能 | はい — null許容のLiveData<String?>経由 |
| 融合(Conflation) | 融合される — 中間値をスキップ | postValue()経由のみ |
| テスト | runTest + Turbineまたは組み込みオペレーター | InstantTaskExecutorRule + observeForever |
StateFlowはView層で明示的なサブスクリプション管理が必要です。Fragment/Activityでは、repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }でサブスクリプションを行います。これはLiveDataの自動サブスクリプションよりも制御が効きますが、ボイラープレートコードが増えます。Jetpack Composeでは、val state by viewModel.uiState.collectAsState()に簡略化されます。
Googleの推奨(Android Developers、2025):新しいKotlinプロジェクトでは、特にComposeを使用する場合にStateFlowを使用してください。LiveDataは次の場合に残します:(1)Javaコード、(2)Java互換性が必要なライブラリ、(3)Room DAO(DAOの戻り値の型としてのLiveDataは依然として人気があります)。
MutableStateFlowはStateFlowのミュータブルバージョンで、書き込み用にvalueプロパティを公開しています。MutableLiveDataと同様に、MutableStateFlowはViewModel内部で使用され、外部のサブスクライバーにはStateFlow(読み取り専用)として公開されます。
class TimerViewModel : ViewModel() {
private val _seconds = MutableStateFlow(0)
val seconds: StateFlow<Int> get() = _seconds
private val _isRunning = MutableStateFlow(false)
val isRunning: StateFlow<Boolean> get() = _isRunning
private var job: Job? = null
fun start() {
if (_isRunning.value) return
_isRunning.value = true
job = viewModelScope.launch {
while (_isRunning.value) {
delay(1000)
_seconds.value++
}
}
}
fun stop() {
_isRunning.value = false
job?.cancel()
}
}
MutableStateFlowの特徴:(1)値は常に非null — コンストラクタによる初期化が必要;(2)equals()による古い値と新しい値の比較 — 新しい値が古い値と等しい場合、サブスクライバーは通知されません;(3)valueへの書き込みは任意のスレッドから可能ですが、CAS操作のために呼び出しスレッドを一時的にブロックするだけです。Kotlin Coroutinesのドキュメントによると、equals()による比較により、LiveDataと比較して不要な通知が90%削減され、高い更新頻度でパフォーマンスが向上します。
ViewModelでStateFlowを使用する場合は、次のルールに従ってください:(1)ViewModel内部ではprivate修飾子でMutableStateFlowを使用する;(2)get()で読み取り専用のStateFlowを公開する;(3)複雑な画面ではシールドクラスを状態型として使用する;(4)現在の値と等しい値を発行しない(StateFlowは自動的に行います)。
// 推奨される画面状態構造
sealed interface ProfileState {
data object Loading : ProfileState
data class Success(
val name: String,
val email: String,
val avatarUrl: String
) : ProfileState
data class Error(val message: String) : ProfileState
}
class ProfileViewModel : ViewModel() {
private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
val state: StateFlow<ProfileState> get() = _state
fun loadProfile(userId: String) {
viewModelScope.launch {
_state.value = ProfileState.Loading
try {
val profile = repository.getProfile(userId)
_state.value = ProfileState.Success(
name = profile.name,
email = profile.email,
avatarUrl = profile.avatarUrl
)
} catch (e: Exception) {
_state.value = ProfileState.Error(e.message ?: "Unknown error")
}
}
}
}
単一の状態型としてシールドクラスを使用することは、Googleが推奨するアプローチです(UDF — Unidirectional Data Flow)。これにより、UIが常に一貫した状態(Loading、Success、Errorのいずれかであり、同時には発生しない)にあることが保証されます。IT Sectrでは、2022年にすべての画面でStateFlow + シールドクラスに移行しました。これにより、予測可能な状態によりViewModelのテストが40%簡素化されました。
stateIn()はコールドFlowをホットStateFlowに変換するオペレーターです。CoroutineScope(内部コルーチンが実行される場所)とSharingStarted戦略を指定する必要があります。SharingStartedの適切な選択は、StateFlowのパフォーマンスとライフサイクルに重大な影響を与えます。
// SharingStartedの3つの戦略:
// 1. SharingStarted.Eagerly — 即座に開始、停止しない
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — 最初のサブスクライバーで開始、停止しない
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — サブスクライバーが存在するときに開始、
// 最後のサブスクライバーが去った後、stopTimeoutMillis(デフォルト0)後に停止
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — ViewModelに最適な戦略:最後のサブスクライバーが去った後も、内部コルーチンはさらに5秒間動作し続けます。この間にユーザーが画面に戻ると、フローを再起動せずにサブスクリプションが復元されます。タイムアウトにより、画面の高速切り替え時の頻繁な再起動を防ぎます。Googleのテスト(Android Performance、2024)によると、WhileSubscribedを5秒のタイムアウトで使用すると、Eagerlyと比較してCPU消費が25%削減されます。
検索クエリ、結果、ローディング状態を備えた本格的な検索画面。ViewModelは、Composeとのリアクティブ通信にシールドクラスUIStateとStateFlowを使用します。
sealed interface SearchUiState {
data object Empty : SearchUiState
data object Loading : SearchUiState
data class Results(val items: List<Product>) : SearchUiState
data class Error(val message: String) : SearchUiState
}
class SearchViewModel constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> get() = _searchQuery
private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
val uiState: StateFlow<SearchUiState> get() = _uiState
init {
viewModelScope.launch {
_searchQuery
.debounce(300)
.filter { it.length >= 3 }
.flatMapLatest { query ->
_uiState.value = SearchUiState.Loading
repository.searchProducts(query)
}
.collect { products ->
_uiState.value = SearchUiState.Results(products)
}
}
}
fun onQueryChanged(query: String) {
_searchQuery.value = query
}
}
// Composeの場合:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... Loading、Results、Errorの状態に反応するUI
}
Room(バージョン2.4.0以降)は、DAOからFlowを返すことをサポートしています。combineを使用して複数のFlowを結合することは、複雑な画面のための強力なパターンです。
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE status = :status")
fun getOrdersByStatus(status: String): Flow<List<Order>>
}
class OrderViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).orderDao()
val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val summary: StateFlow<OrderSummary> = combine(
dao.getOrdersByStatus("active"),
dao.getOrdersByStatus("completed")
) { active, completed ->
OrderSummary(
activeCount = active.size,
completedCount = completed.size,
totalAmount = (active + completed).sumOf { it.amount }
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}
Roomはordersテーブルの変更を自動的に追跡し、変更があるとデータを再クエリします。StateFlow + Roomは、Room + LiveDataの最新の代替です。Google(Android Architecture Guide、2025)によると、Flow + StateFlow + Roomのスタックは、データベース変更時のリアクティブなUI更新が必要なすべてのKotlinプロジェクトに推奨されます。
よくある質問
融合は、StateFlowが最後に送信された値のみを保持するメカニズムです。サブスクライバーが前の値を処理する前に新しい値が送信されると、中間の値は失われます。これはUIにとって重要です。状態がLoading → Success → Errorと変化し、UIがSuccessをレンダリングしていない場合、余分なレンダリングなしで直接Errorに移行します。融合は、Composeでの過剰な再コンポジションを防ぐAndroidの重要な最適化です。
lifecycle-livedata-ktxライブラリの拡張関数liveData.asFlow()を使用し、次に.stateIn()でStateFlowに変換します。逆変換はstateFlow.asLiveData()です。変換はLiveDataからStateFlowへの移行時に役立ちます。古いViewをLiveDataでサブスクライブしたまま、ViewModelを順次StateFlowに変換できます。
StateFlowは常に値を持つ必要があります — これがインターフェースの契約です:新しく接続したサブスクライバーは、待つことなく即座に現在の状態を受け取ります。初期値はMutableStateFlow(initialValue)コンストラクタまたはstateIn(initialValue)オペレーターに渡されます。状態が存在しない可能性がある場合は、null許容型でMutableStateFlow<T?>(null)を使用し、UIでnullを処理します。
はい、StateFlowはスレッドセーフです。valueの読み取りと書き込みはアトミック操作(CAS)を使用します。ただし、collect()はsuspend関数であり、コルーチン内で起動する必要があります。発行と収集が異なるスレッドで発生する場合、StateFlowはvalueに対するすべての操作のhappens-beforeを保証します。ViewでStateFlowを収集するには、lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }を使用します。
厳密な制限はありませんが、画面ごとに3〜5個以上の個別のStateFlowを使用しないことをお勧めします。より多くの異なる状態が必要な場合は、シールドクラスまたはデータクラスを使用して1つに結合します。各StateFlowは収集時にContinuationオブジェクトの割り当てを必要とし、100個のStateFlowはGCに顕著な負荷をかける可能性があります。Googleの推奨によると、画面ごとに1つのシールドクラスUIStateが、読みやすさとパフォーマンスの最適なバランスです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。