はじめに
Android開発を続けていると、「リアルタイムでデータが更新される画面を作りたい」「検索機能で入力に応じて即座に結果を表示したい」といった要求に直面することがよくあります。
従来のCallback地獄やRxJavaの複雑さに悩んでいた方も多いのではないでしょうか?Kotlin Flowを使えば、これらの問題をエレガントに解決できます。
この記事では、実際の開発現場で役立つKotlin Flowの使い方を、具体的なコード例とともに解説していきます。
前提知識: Kotlin Coroutinesの基本(suspend関数、viewModelScope等)を理解していることを前提としています。
Kotlin Flowって何?なぜ必要なの?
まず、Kotlin Flowがどんなものかを理解しましょう。
従来のsuspend関数は「1回だけ値を返す」非同期処理でした。一方、Flowは「時間をかけて複数の値を順次流す」データストリームです。
// Suspend関数の場合:1回だけ値を返す
suspend fun fetchUserData(): User {
delay(1000) // API呼び出しをシミュレート
return User(name = "太郎")
}
// Flowの場合:複数の値を時間をかけて流す
fun userUpdatesStream(): Flow<User> = flow {
emit(User(name = "太郎", status = "オンライン"))
delay(2000)
emit(User(name = "太郎", status = "離席中"))
delay(3000)
emit(User(name = "太郎", status = "オフライン"))
}
リアルタイムチャットアプリやライブデータの表示など、「継続的に変化するデータ」を扱う場面でFlowが威力を発揮します。
実際の開発でよく使うFlowオペレーター
開発現場でよく遭遇するパターンを見てみましょう。
検索機能の実装
ユーザーが検索ボックスに文字を入力するたびにAPI呼び出しを行うのは非効率ですよね。
// 入力に応じてリアルタイム検索
val searchQuery = MutableStateFlow("")
val searchResults = searchQuery
.debounce(300) // 300ms間隔で入力を安定化
.distinctUntilChanged() // 同じクエリは無視
.filter { it.length >= 2 } // 2文字以上で検索
.flatMapLatest { query -> // 最新の検索のみ実行
if (query.isEmpty()) {
flowOf(emptyList())
} else {
searchApi(query)
}
}
データの変換とフィルタリング
実際の開発では、取得したデータをそのまま使うことは稀です。必要な形に変換したり、条件でフィルタリングしたりする必要があります。
よく使うパターン:
// APIから取得したユーザーリストを画面表示用に変換
val userDisplayList = userRepository.getAllUsers()
.map { users ->
users.map { user ->
UserDisplayItem(
name = user.name,
statusText = if (user.isOnline) "オンライン" else "オフライン",
avatarUrl = user.profileImageUrl ?: DEFAULT_AVATAR
)
}
}
.filter { it.isNotEmpty() } // 空のリストは表示しない
// 特定の条件でフィルタリング
val activeUsers = userFlow
.filter { user -> user.isActive && user.lastLoginTime > yesterday }
// 重複を除去(連続する同じ値を無視)
val uniqueSearchResults = searchResultFlow
.distinctUntilChanged()
タイミングをコントロールする
パフォーマンスを考慮すると、すべてのイベントをそのまま処理するのは現実的ではありません。適切にタイミングを調整することが重要です。
頻繁に発生するイベントを適切に処理する:
// 検索入力:ユーザーの入力が落ち着いてから検索実行
val searchInput = MutableStateFlow("")
val debouncedSearch = searchInput
.debounce(300) // 300ms待ってから処理
.distinctUntilChanged() // 同じ値は無視
// 位置情報:一定間隔で最新の位置のみ取得
val locationUpdates = locationFlow
.sample(1000) // 1秒間隔で最新の位置を取得
// ボタンクリック:連続クリックを防止
val buttonClicks = buttonClickFlow
.throttleFirst(1000) // 1秒間は最初のクリックのみ有効
複数のデータソースを組み合わせる
実際のアプリでは、複数のデータソースからの情報を組み合わせて画面を構築することがよくあります。
実用的な組み合わせパターン:
// ユーザー情報と設定情報を組み合わせて画面状態を作成
val uiState = combine(
userRepository.getCurrentUser(),
settingsRepository.getUserSettings(),
networkMonitor.isOnline()
) { user, settings, isOnline ->
UiState(
user = user,
isDarkMode = settings.isDarkMode,
canSync = isOnline && user != null
)
}
// 複数のAPIレスポンスを並行して取得してマージ
val allNotifications = merge(
pushNotificationFlow,
inAppNotificationFlow,
systemNotificationFlow
).distinctUntilChanged()
StateFlow、SharedFlow、Flowの使い分け
Flowには3つの主要な種類があり、それぞれ異なる用途に適しています。開発現場でどれを選ぶべきか迷うことも多いのではないでしょうか。
使い分けの指針
StateFlow: 状態を管理したい場合(最新の値を保持)
// ViewModelでの画面状態管理
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
SharedFlow: 一回限りのイベント(ナビゲーション、スナックバー表示など)
// 一回限りのイベント管理
class UserViewModel : ViewModel() {
private val _events = MutableSharedFlow<UserEvent>()
val events: SharedFlow<UserEvent> = _events.asSharedFlow()
}
通常のFlow: データストリーム(API呼び出し、センサー値など)
// API呼び出しやデータベース監視
fun getUserUpdates(): Flow<User> = flow {
while (true) {
emit(fetchUserFromApi())
delay(30000) // 30秒間隔で更新
}
}
実践的な実装パターン
ネットワーク状態とローディング管理
よくあるのは、「APIの呼び出し中はローディング表示、成功したらデータ表示、失敗したらエラー表示」というパターンです。
class UserRepository(private val apiService: ApiService) {
fun fetchUser(userId: String): Flow<UiState<User>> = flow {
emit(UiState.Loading) // まずはローディング状態
try {
val user = apiService.getUser(userId)
emit(UiState.Success(user)) // 成功時
} catch (e: Exception) {
emit(UiState.Error(e.message ?: "不明なエラー")) // エラー時
}
}
}
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
データベースとAPI連携のベストプラクティス
Roomデータベースと組み合わせると、オフライン対応の実装が簡単になります。
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserFlow(userId: String): Flow<User?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
}
class UserRepository(
private val userDao: UserDao,
private val apiService: ApiService
) {
// ①データベースを唯一の信頼できる情報源とする(Single Source of Truth)
fun getUser(userId: String): Flow<User?> = userDao.getUserFlow(userId)
// ②データ更新は別の関数で
suspend fun refreshUser(userId: String) {
try {
val networkUser = apiService.getUser(userId)
userDao.insertUser(networkUser) // DBが更新されると、①のFlowが自動的に最新データをemitする
} catch (e: Exception) {
// エラーハンドリング
throw e
}
}
}
開発時に遭遇しがちな問題と対策
状態が意図せず変更される
MutableStateFlow
を直接公開してしまい、外部から状態が変更されてしまう問題がありました。
// ❌ 外部から変更可能
val uiState = MutableStateFlow(UiState.Loading)
// ✅ 読み取り専用で公開
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
おわりに
Kotlin Flowを使い始めた当初は理解に苦労しましたが、一度慣れてしまうと手放せない技術になりました。特に、リアルタイムデータの処理やUI状態管理において、従来のCallback方式と比較して圧倒的に読みやすく保守しやすいコードが書けるようになります。
最初は簡単な検索機能から始めて、徐々に複雑な状態管理にも適用していくことをお勧めします。皆さんの開発がより効率的になることを願っています!