気象庁の天気予報を取得するアプリを作ろう #1
JetpackComposeで、いろいろライブラリを触ってみたかったので、気象庁の天気予報を表示するアプリを作ってみようと思います。
使用するライブラリとか技術とか
以下のライブラリ、記述を使用します。
- JetpackCompose
- Retrofit
- Hilt
- MVVM
- kotlin coroutine
気象庁のAPIについて
気象庁が提供している以下のAPI使用して、天気予報を表示するアプリを作成しようと思います。
気象庁の天気APIの元ネタはこちらです。
地域情報:https://www.jma.go.jp/bosai/common/const/area.json
気象情報:https://www.jma.go.jp/bosai/forecast/data/forecast/xxxxxx.json
地域情報のarea.jsonで取得できる[offices]の要素のkeyを、気象情報のxxxxxx.jsonのxxxxxxに指定して使用します。
例)群馬県なら「https://www.jma.go.jp/bosai/forecast/data/forecast/100000.json」こんな感じ
作成の青写真
以下の順番で作成を進めていきます。
- Hiltを導入する
- Retrofitを使用して、APIを定義する
- coroutineを使用して、Repositoryを作成する
- ViewModelを作成する
- 画面を作る
Hiltの導入
まずは、gradleの定義から。
プロジェクトレベルのbuild.gradleに以下を追加します。
plugins {
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
モジュール(app)レベルのbuild.gradleに以下を追加します。
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
:
android {
}
dependencies {
// 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'
}
kapt {
correctErrorTypes true
}
gradleのSyncNowを行ったら、次にカスタムApplicationクラス(ForecastApplicationという名前にしました。)を作成して、AndroidManifest.xmlに追加します。
@HiltAndroidApp
class ForecastApplication : Application()
<application
android:name=".ForecastApplication"
:
>
</application>
Hiltは、モジュールを作成していく中で成長させていくので、いったん終了です。
Retrofit
モジュール(app)レベルのbuild.gradleにRetrofitのライブラリ定義を追加します。
Retrofitに、Gsonを使用してJson文字列とオブジェクトの変換までやってもらうつもりです。
// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// gson
implementation 'com.google.code.gson:gson:2.10.1'
まずは、気象庁の地点情報を取得するので、Gsonが解析できるように地域情報モデル(Area.kt)を定義します。
すべてのコードはこちらをご覧ください。
data class Area(
val centers: Map<String, Center>,
val offices: Map<String, Office>,
val class10s: Map<String, Class10>,
val class15s: Map<String, Class15>,
val class20s: Map<String, Class20>,
)
:
次に、APIのREST定義を行います。
interface ForecastApi {
@GET("common/const/area.json")
suspend fun getArea(): Response<Area>
}
これをHilt経由で注入できるようにModule定義します。
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
@Singleton
@Provides
fun provideForecastApi(
retrofit: Retrofit,
): ForecastApi =
retrofit.create(ForecastApi::class.java)
@Singleton
@Provides
fun provideGson() =
GsonBuilder().create()
@Singleton
@Provides
fun provideHttpClient() =
OkHttpClient.Builder()
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build()
@Singleton
@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
gson: Gson,
): Retrofit =
Retrofit.Builder()
.baseUrl("https://www.jma.go.jp/bosai/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
ViewModelとcoroutine
RetrofitのAPIを呼び出すためのRepository、
それを使用するUseCase、
画面のViewModel
の3つを作っていきます。
まずは、Repositoryからです。
API呼び出しは非同期で行いますので、flowを使用してIOスレッドで行います。
その準備として、APIをflowで呼び出すためのwrapperを作成します。
inline fun <reified T : Any> apiFlow(crossinline call: suspend () -> Response<T>): Flow<Future<T>> =
flow<Future<T>> {
val response = call()
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.IO)
次にこれを利用するRepositoryを作成します。
ここでは、Repositoryクラスに直接@Singletonしていますが、ApiModule.ktに記述してもいいと思います。
@Singleton
class ForecastRepository @Inject constructor(
private val forecastApi: ForecastApi,
) {
fun getArea(): Flow<Future<Area>> = apiFlow { forecastApi.getArea() }
}
Repositoryを利用するUseCaseを作成します。
interface GetAreaUseCase {
suspend fun invoke(): Flow<Future<Area>>
}
class GetAreaUseCaseImpl @Inject constructor(
private val forecastRepository: ForecastRepository,
) : GetAreaUseCase {
override suspend fun invoke(): Flow<Future<Area>> {
return forecastRepository.getArea()
}
}
これをDIする定義をApiModule.ktに追加します。
@Singleton
@Provides
fun provideGetAreaUseCase(
forecastRepository: ForecastRepository,
): GetAreaUseCase = GetAreaUseCaseImpl(forecastRepository)
最後にViewModelを定義します。
@HiltViewModel
class ForecastViewModel @Inject constructor(
private val getAreaUseCase: GetAreaUseCase,
) : ViewModel() {
var areaFuture: Future<Area> by mutableStateOf(Future.Proceeding)
var centerIndex: Int by mutableStateOf(1)
init {
refreshArea()
}
fun refreshArea() {
viewModelScope.launch {
areaFuture = Future.Proceeding
getAreaUseCase.invoke().collectLatest {
areaFuture = it
if (it is Future.Success) {
centerIndex = 0
}
}
}
}
}
Futureの部分をmutableStateにするか、StateFlowにするか、散々迷ったのですが、Googleのコードラボにしたがって、mutableStateOfでの定義にしました。
画面を作る
最後に画面を作って、地域情報の表示は完成です。
ActivityにDIできるように@AndroidEntryPointを記述します。
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
WeatherForecastTheme {
ForecastScreen()
}
}
}
}
ViewModelをDIしたいので、hiltViewModel()を記述します。
あとは、areaFutureの状態に応じた画面を表示すればOKです。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ForecastScreen(
viewModel: ForecastViewModel = hiltViewModel(),
) {
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
val areaState = viewModel.areaFuture
val modifier = Modifier
.padding(paddingValues = paddingValues)
.fillMaxSize()
when (areaState) {
is Future.Proceeding -> LoadingScreen(modifier)
is Future.Error -> ErrorScreen(modifier, viewModel)
is Future.Success -> DataScreen(modifier, viewModel, areaState.value)
}
}
}
いざ実行
実行すると、以下の様な画面が表示されます。(全コードはこちら)
「北海道地域」という部分が、気象庁のAPIを使用して取得した文字列になります。
「検索」ボタンは、とりあえずつけてみただけで、まだ動かないです。(色も変えないと何だかわかんないですね。。。)
次回予告
次回は「北海道地域」ボタンを押すと、リスト選択ダイアログを使用して別に地域が選択できるようにする。
もう一つ詳細な地域もボタン表示する。
「検索」ボタンを押すと、天気予報を表示する(完成!!)を記載する予定です。