What You'll Learn
Flow/StateFlow実践パターン(combine、flatMapLatest、debounce、retry、callbackFlow、テスト)を解説します。
combine: Combining Multiple Flows
@HiltViewModel
class DashboardViewModel @Inject constructor(
userRepository: UserRepository,
orderRepository: OrderRepository,
notificationRepository: NotificationRepository
) : ViewModel() {
val dashboardState = combine(
userRepository.getUser(),
orderRepository.getRecentOrders(),
notificationRepository.getUnreadCount()
) { user, orders, unreadCount ->
DashboardState(user, orders, unreadCount)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), DashboardState())
}
flatMapLatest: Search
@HiltViewModel
class SearchViewModel @Inject constructor(
private val repository: SearchRepository
) : ViewModel() {
private val _query = MutableStateFlow("")
val results = _query
.debounce(300) // 300ms待ってから検索
.distinctUntilChanged()
.flatMapLatest { query ->
if (query.isBlank()) flowOf(emptyList())
else repository.search(query)
}
.catch { emit(emptyList()) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun onQueryChange(query: String) { _query.value = query }
}
retry: Error Retry
fun getDataWithRetry(): Flow<List<Item>> = flow {
emit(api.getItems())
}.retry(3) { cause ->
cause is IOException && run {
delay(1000)
true
}
}.catch { emit(emptyList()) }
// exponential backoff
fun <T> Flow<T>.retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 1000
): Flow<T> = retryWhen { cause, attempt ->
if (attempt < maxRetries && cause is IOException) {
delay(initialDelay * (1 shl attempt.toInt()))
true
} else false
}
callbackFlow: Callback to Flow Conversion
fun locationUpdates(context: Context): Flow<Location> = callbackFlow {
val client = LocationServices.getFusedLocationProviderClient(context)
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000).build()
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { trySend(it) }
}
}
client.requestLocationUpdates(request, callback, Looper.getMainLooper())
awaitClose { client.removeLocationUpdates(callback) }
}
stateIn vs shareIn
// stateIn: 最新値を保持(UI状態向け)
val uiState = flow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 5秒キャッシュ
initialValue = UiState.Loading
)
// shareIn: イベントストリーム(通知向け)
val events = flow.shareIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
replay = 0 // 新しいサブスクライバに過去のイベントを送らない
)
Testing (Turbine)
@Test
fun searchReturnsResults() = runTest {
val viewModel = SearchViewModel(FakeSearchRepository())
viewModel.results.test {
assertEquals(emptyList<SearchResult>(), awaitItem()) // 初期値
viewModel.onQueryChange("kotlin")
advanceTimeBy(300) // debounce待ち
val results = awaitItem()
assertTrue(results.isNotEmpty())
}
}
Summary
| パターン | 用途 |
|---|---|
combine |
Combine multiple flows |
flatMapLatest |
Process latest only |
debounce |
Wait for input |
retry |
Error retry |
callbackFlow |
Callback conversion |
stateIn |
Share UI state |
-
combineでIntegrate multiple data sources -
debounce+flatMapLatestでSearch optimization -
callbackFlowでLegacy API to Flow -
WhileSubscribed(5000)でBackground optimization
8種類のAndroidAppTemplates(FlowPre-designed)を公開しています。
Template List → Gumroad
Related Articles:
Ready-Made Android App Templates
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
Top comments (0)