注意
本記事は、
筆者個人の学習内容を整理するためのアウトプット
としてまとめたものです。
マサカリ歓迎します。
1-1. 概要
OpenWeatherAPIで東京の天気データを取得し、
Viewに表示するまでの流れをサンプルコード付きで記載してます。
アーキテクチャはAndroid MVVM + MVI(UDF)になります。
1-2. 全体構成図
View (Compose)
↓
ViewModel
↓
UseCase(機能単位で1つ)
↓
Repository
↓
OpenWeather API
1-3. 設計方針
以下に基づき、サンプルコードを含めて記載します。
・Viewは状態を表示するだけ
・ViewModelは状態遷移を管理する
・Stateはsealed + Factory で生成する
・UseCaseはfeature単位で1つ
2-1. OpenWeather API(Model)
data class WeatherResponse(
val weather: List<Weather>,
val main: Main
)
data class Weather(
val main: String,
val description: String
)
data class Main(
val temp: Double
)
2-2. Repository
interface WeatherRepository {
suspend fun fetchTokyoWeather(): WeatherResponse
}
class WeatherRepositoryImpl(
private val api: OpenWeatherApi
) : WeatherRepository {
override suspend fun fetchTokyoWeather(): WeatherResponse {
return api.getWeather(
city = "Tokyo",
apiKey = BuildConfig.OPEN_WEATHER_API_KEY
)
}
}
2-3. UseCase
class LoadTokyoWeatherUseCase(
private val repository: WeatherRepository
) {
suspend operator fun invoke(): WeatherResponse {
return repository.fetchTokyoWeather()
}
}
2-4. State(Factoryメソッド使用)
sealed interface WeatherState {
object Idle : WeatherState
object Loading : WeatherState
data class Success(
val description: String,
val temp: Double
) : WeatherState
data class Error(
val message: String
) : WeatherState
companion object {
fun idle() = Idle
fun loading() = Loading
fun success(response: WeatherResponse) =
Success(
description = response.weather.first().description,
temp = response.main.temp
)
fun error(message: String) =
Error(message)
}
}
State Factory メソッド化の理由
・状態生成ロジックをViewModelから排除
・copy の引数漏れを防止
・状態遷移を明文化
Stateの生成をFactoryメソッドに集約することで、
ViewModelは「いつ遷移するか」だけを判断する役割になります。
2-5. ViewModel(reduce + Factory)
class WeatherViewModel(
private val loadTokyoWeatherUseCase: LoadTokyoWeatherUseCase
) : ViewModel() {
private val _state =
MutableStateFlow<WeatherState>(WeatherState.idle())
val state: StateFlow<WeatherState> = _state
fun loadWeather() {
viewModelScope.launch {
reduce { WeatherState.loading() }
val result = runCatching {
loadTokyoWeatherUseCase()
}
reduce {
result.fold(
onSuccess = {
WeatherState.success(it)
},
onFailure = {
WeatherState.error("天気情報の取得に失敗しました")
}
)
}
}
}
private fun reduce(
reducer: () -> WeatherState
) {
_state.value = reducer()
}
}
2-6. View(Compose)
@Composable
fun WeatherScreen(
viewModel: WeatherViewModel
) {
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadWeather()
}
when (state) {
is WeatherState.Idle -> Text("準備中")
is WeatherState.Loading -> CircularProgressIndicator()
is WeatherState.Success -> {
val success = state as WeatherState.Success
Text(
text = "東京の天気: ${success.description}\n気温: ${success.temp}℃"
)
}
is WeatherState.Error -> {
Text("エラーが発生しました")
}
}
}
まとめ
MVVM と MVI は対立するものではなく、
State管理をどう設計するかの違いだと考えています。
reduce経由で状態を一つにまとめてViewに返却する設計にすることで
複数のデータ取得時に状態が複雑化するのを防ぎ、運用面での恩恵が強いと考えています。