Flow (-> MutableStateFlow) -> StateFlow
UseCaseでFlowを作って、ViewModelでStateFlowにして、Screenで表示する方法を学習しようと思います。
結論は、「UseCaseのFlowをそのままstateIn()で流すのはよろしくない。」でした。
改造前のソース
APIリポジトリ
fun getAreaFlow(): Flow<Future<AreaApiModel>> {
return flow<Future<AreaApiModel>> {
val response = forecastApi.getArea()
if (response.isSuccessful) {
emit(Future.Success(value = response.body()!!))
} else {
throw HttpException(response)
}
}.catch { cause ->
emit(Future.Error(cause))
}.onStart {
emit(Future.Proceeding)
}.flowOn(dispatchers)
}
独自定義クラス(FutureとforecastApi)の説明はこちら
forecastApi.getArea()は、RetrofitのInterfaceです。
interface ForecastApi {
@GET("common/const/area.json")
suspend fun getArea(): Response<AreaApiModel>
}
Futureは、API呼び出しの状態を保持しているモデルです。
sealed class Future<out T> {
object Idle : Future<Nothing>()
object Proceeding : Future<Nothing>()
data class Success<out T>(val value: T) : Future<T>()
data class Error(val error: Throwable) : Future<Nothing>()
}
APIリポジトリを呼び出しているUseCase
class GetAreaUseCaseImpl @Inject constructor(
private val forecastRepository: ForecastRepository,
) : GetAreaUseCase {
override fun invoke(): Flow<Future<Area>> {
return forecastRepository.getArea().map { apiModelFuture ->
when (apiModelFuture) {
is Future.Error -> Future.Error(apiModelFuture.error)
is Future.Success -> Future.Success(AreaAdapter.adaptFromApi(apiModelFuture.value))
is Future.Proceeding -> Future.Idle
is Future.Idle -> Future.Idle
}
}
}
}
UseCaseを使用しているViewModel
private var _areaFutureState: MutableStateFlow<Future<Area>> = MutableStateFlow(Future.Idle)
val areaFutureStateFlow: StateFlow<Future<Area>> = _areaFutureState.asStateFlow()
:
init {
refreshArea()
}
fun refreshArea() {
_areaFutureState.value = Future.Proceeding
viewModelScope.launch {
getAreaUseCase().collect {
_areaFutureState.value = it
if (it is Future.Success) {
_center.value = it.value.centers.firstOrNull()
_office.value = _center.value?.offices?.firstOrNull()
}
}
}
}
ViewModelを使っているScreen
val areaState = viewModel.areaFutureStateFlow.collectAsState()
改造
FlowをStateFlowとしてそのまま使うだけなら以下でいいんですよね。
- private var _areaFutureState: MutableStateFlow<Future<Area>> = MutableStateFlow(Future.Idle)
- val areaFutureStateFlow: StateFlow<Future<Area>> = _areaFutureState.asStateFlow()
+ val areaFutureStateFlow: StateFlow<Future<Area>> = getAreaUseCase()
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Lazily,
+ initialValue = Future.Idle
+ )
- init {
- refreshArea()
- }
- fun refreshArea() {
- _areaFutureState.value = Future.Proceeding
-
- viewModelScope.launch {
- getAreaUseCase().collect {
- _areaFutureState.value = it
- if (it is Future.Success) {
- _center.value = it.value.centers.firstOrNull()
- _office.value = _center.value?.offices?.firstOrNull()
- }
- }
- }
- }
ここで、3つの疑問が浮上しました。
- データ取得ができたときに、_center.valueと_office.valueに初期値を入れていたがこの処理をどうする?
- これ、UseCase側が初期値を欲したときにどうするんだ?
- 再取得処理ってどうやるんだ?
また改造
初期値を入れてみようということで、ぱぱっと力業?でやるとこうなるかなというのを書いてみました。
val areaFutureStateFlow: StateFlow<Future<Area>> = getAreaUseCase()
+ .map { areaFuture ->
+ if (areaFuture is Future.Success) {
+ _center.value = areaFuture.value.centers.firstOrNull()
+ _office.value = _center.value?.offices?.firstOrNull()
+ }
+ areaFuture
+ }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = Future.Idle
)
mapで入れちゃおう作戦です。とりあえず動きますが、力業感が否めない。。。
他のStateの状態に依存して、自分の状態を変える的な何かが絶対あるはずと信じて探してみます。
...う〜ん...ない!(誰か教えてください (・・)(..)ペコッ)
一番多かったのが、uiStateを管理するモデルを作って、不整合を起こりにくくした形にしてmap{}で変換しようというものでした。
まずは2つモデルを作ります。
/** Screenで使用するモデル */
data class ForecastUiState(
val areaFuture: Future<Area> = Future.Idle,
val center: Center? = null,
val office: Office? = null,
)
/** ViewModelで値を変えていくモデル */
class ForecastViewModelState(
val areaFuture: Future<Area> = Future.Idle,
val center: Center? = null,
val office: Office? = null,
) {
fun toUiState(): ForecastUiState {
when (areaFuture) {
is Future.Idle,
is Future.Proceeding,
is Future.Error -> {
return ForecastUiState(
areaFuture = areaFuture,
center = null,
office = null,
)
}
is Future.Success -> {
val center = this.center ?: areaFuture.value.centers.firstOrNull()
return ForecastUiState(
areaFuture = areaFuture,
center = center,
office = this.office ?: center?.offices?.firstOrNull()
)
}
}
}
}
+ private val _areaFutureStateFlow: MutableStateFlow<ForecastViewModelState> = MutableStateFlow(ForecastViewModelState())
- val areaFutureStateFlow: StateFlow<Future<Area>> = getAreaUseCase()
- .map { areaFuture ->
- if (areaFuture is Future.Success) {
- _center.value = areaFuture.value.centers.firstOrNull()
- _office.value = _center.value?.offices?.firstOrNull()
- }
- areaFuture
- }
+ val areaFutureStateFlow: StateFlow<ForecastUiState> = _areaFutureStateFlow
+ .map { it.toUiState() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
- initialValue = Future.Idle
+ initialValue = _areaFutureStateFlow.value.toUiState(),
)
+ /** init復活 */
+ init {
+ refreshArea()
+ }
+ /** refreshAreaも復活 */
+ fun refreshArea() {
+ _areaFutureStateFlow.value = ForecastViewModelState(areaFuture = Future.Proceeding)
+
+ viewModelScope.launch {
+ getAreaUseCase().collect {
+ _areaFutureStateFlow.value = ForecastViewModelState(areaFuture = it)
+ }
+ }
+ }
uiStateというモデルが増えただけで、結局改造前に戻りました。
この作りだと、
- これ、UseCase側が初期値を欲したときにどうするんだ?
- 再取得処理ってどうやるんだ?
という他の疑問も解決です。
やっぱりUseCaseのFlowをそのままScreenに出すのは現実的じゃないのかな。
誰か教えてください (・・)(..)ペコッ
完成ソース
完成ソースはこちらです。