気象庁の天気予報を取得するアプリを作ろう #3
前回は、地名を選択すると選択ダイアログを表示するところまで作成しました。
今回は、OfficeId(code?)で、天気予報を取得します。
使用するライブラリ
前回から何も変化はなしです。
// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2'
runtimeOnly 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
// gson
implementation 'com.google.code.gson:gson:2.10.1'
// hilt
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
使用するAPI
以下の気象庁のAPIを使用して、天気予報を取得します。
090000の部分を、取得したい地域のOfficeId(code?)にすることで、その地域の天気予報が取得できます。
curl -X GET https://www.jma.go.jp/bosai/forecast/data/forecast/090000.json
Retrofit
APIの定義を追加します。
可変となるAPIのパスは、@Pathで指定します。
Gsonを使用してjsonをパースする先のオブジェクトとして、ForecastApiModelを作成しました。
@GET("forecast/data/forecast/{office-id}.json")
suspend fun getForecast(
@Path("office-id") officeId: String,
): Response<List<ForecastApiModel>>
API-Repository
APIを呼び出すForecastRepositoryに、定義したAPIを追加します。
fun getForecast(officeId: String): Flow<Future<List<ForecastApiModel>>> {
return apiFlow { forecastApi.getForecast(officeId = officeId) }
}
モデルとadapterの定義
このAPIを使用するユースケースを作成します。がその前に、、、
後々、API経由でデータを取得した後に、Roomでストレージに保存して、10分くらいはストレージから取得したデータを使うようにしようと思ってまして、以下の様な構成を考えております。
- APIのモデル - Adapter - アプリ内部のモデル
- Roomのモデル - Adapter - アプリ内部のモデル
今回はRoomのことは考えず、アプリ内部用のForecastモデルと、APIの生データを流し込むForecastApiModel、変換アダプタのForecastAdapterクラスを作成します。
UseCase
OfficeIDをキーにして、APIから天気予報を取得するユースケースを作成します。
interface GetForecastUseCase {
suspend fun invoke(office: Office): Flow<Future<Forecast>>
}
class GetForecastUseCaseImpl @Inject constructor(
private val forecastRepository: ForecastRepository,
) : GetForecastUseCase {
override suspend fun invoke(office: Office): Flow<Future<Forecast>> {
return forecastRepository.getForecast(officeId = office.id).map { apiModelFuture ->
when (apiModelFuture) {
is Future.Error -> {
Future.Error(apiModelFuture.error)
}
is Future.Success -> {
Future.Success(ForecastAdapter.adaptFromApi(apiModelFuture.value))
}
is Future.Proceeding -> {
Future.Proceeding
}
else -> {
Future.Idel
}
}
}
}
}
ViewModel
ViewModelに、天気予報のStateを追加します。また、検索を行うと、ユースケース経由でデータを取得する様にします。
var office: Office? by mutableStateOf(null)
private set
var forecastFuture: Future<Forecast> by mutableStateOf(Future.Idel)
private set
:
fun searchForecast() {
val office = office ?: return
viewModelScope.launch {
forecastFuture = Future.Proceeding
getForecastUseCase.invoke(office).collectLatest {
forecastFuture = it
if (it is Future.Success) {
Log.d("Search!!", "${it.value}")
}
}
}
}
Screen
最後に、検索ボタンをタップした際の動作と、天気予報の表示部分を記述します。
ひとまずは、ダラダラっとForecast.toString()が画面に表示される様にします。
Box(
modifier = Modifier.align(Alignment.End),
) {
Button(onClick = {
viewModel.searchForecast()
}) {
Text("検索")
}
}
when (val forecastState = viewModel.forecastFuture) {
is Future.Error -> {
forecastState.error.localizedMessage?.let { Text(it) } ?: Text("天気予報の取得に失敗")
}
is Future.Idel -> {
Text("検索を押してください")
}
is Future.Proceeding -> {
CircularProgressIndicator(
modifier = Modifier.size(60.dp), color = MaterialTheme.colorScheme.primary, strokeWidth = 10.dp
)
}
is Future.Success -> {
Text(forecastState.value.toString())
}
}
いざ実行
検索ボタンを押すと、ダラダラっと結果が表示されるようになりました。
全コードはこちらです