Jetpack ComposeでRetrofit2を使って天気予報アプリを作ってみた

2023.06.11

Jetpack ComposeでOpen Weather のJSONデータから 日本の天気予報アプリを作ったメモ書きです。

概要

都市名を半角ローマ字入力しボタンを押すと入力都市の5日間3時間ごとの天気を表示する。

入力例:那覇市→naha, 愛知県→aichi

準備

OpenWeatherのAPI KEY取得

環境

・ Android Studio: Flamingo 2022.2.1 Patch2

作成

1. 新規プロジェクトの作成

Empty Activityを選択。

Next → name: Weather Forecast Jp で作成しました。

2. 作成環境準備

主なライブラリ:

・Retrofit2

・Gson

・Coil

Gradle SCripts/build.gradle (module : app)のdependencies{}内にライブラリ情報を記述する。

マニュフェストファイルにネットワークについて追記する。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" /> <!---これです-----/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
  -------

3. API から JSON を受け取るための data classを作成

今回のOpen Weather APIのURL:

http://api.openweathermap.org/data/2.5/forecast? q={ 都市名 },jp&units=metric&lang=ja&APPID={ 取得したAPI_KEY }

都市名を toyota で検索

このような JSONデータ を取得しました。

ここから data classを作成するのが大変そうなので、JSON TO Kotlin Class のプラグインを使いました。

File > Settings > Editor/plugins を選択し、JSON TO Kotlin Class で検索しInstallする。

classファイルを入れたいフォルダで右クリック、new > Packageを選択して NEW Packageを作成する。(ここでは weatherstate にしました。)

weatherstateフォルダで右クリック、new > Kotlin data class File From JSONを選択する。

画面内で右クリックして Retrieve content from Http URL を選択する。

先ほどJSONデータを取得した URL を入力する。

OKを押すと JSONデータが画面に表示される。1行横に表示されているので Format ボタンを押す。(しなくてもいいかも)

Class Name: に入力する。(今回は、WeatherItemと入れました。)

Genarate ボタンを押すと weatherstate フォルダ内に data class file が入ります。

確認すると、名前のないファイルが1つ存在していた(ファイル名 .kt)ので、このままでいいかわかりませんが WeatherData とClass名を付けファイル名も同じにしました。

WeatherItem classの val list: List<>を val list: List<WeatherData>にしました。

4. 応答データを保持する data class作成

日時、天気、気温、湿度、気圧、天気イメージの応答データを保持するようにしました。

*このほかにも最低/最高気温、風速etcなどのデータもあります。

Forecast.kt

data class ForecastItems(
    val date: String,  //<----日時
    val description: String,  //<--天気
    val temp: Double, //<--気温 
    val humidity: Int, //<--湿度
    val pressure: Int, //<--気圧
    val icon: String //<--天気Image
)

5. Retrofit作成

ApiService.kt

nterface ApiService {
     @GET("/data/2.5/forecast?")
     suspend fun getWeather(
         @Query("q") cityName: String,
         @Query("units") units: String,
         @Query("lang") lang: String,
         @Query("appid") appId: String
     ): WeatherItem
}

ApiRepos.kt

class ApiRepos {
    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .build()
    }

    fun get(): ApiService{
        val retrofit = getRetrofit()
        return retrofit.create(ApiService::class.java)
    }
}

6. ViewModel 作成

ForecastViewModel.kt

class ForecastViewModel : ViewModel() {
    suspend  fun getForecast(cityName: String, city: MutableState<String>, weather: MutableList<ForecastItems>) {
        weather.clear()
        city.value = ""
        viewModelScope.launch(Dispatchers.IO) {
            val api = ApiRepos().get()
            try {
                val response = api.getWeather(cityName, UNITS, LANG, APIKEY)

                val lists = response.list
                for (listItems in lists) {
                    val dates = listItems.dt_txt
                    val temp = listItems.main.temp
                    val humidity = listItems.main.humidity
                    val pressure = listItems.main.pressure
                    val descriptionLists = listItems.weather
                    for (descriptionItem in descriptionLists) {
                        val description = descriptionItem.description
                        val icon = descriptionItem.icon

                        val iconUrl = "https://openweathermap.org/img/w/$icon.png"

                        weather.add(ForecastItems(dates, description, temp, humidity, pressure, iconUrl))

                    }
                }
                val cities = response.city.name
                city.value = cities

            } catch (e: Exception) {
                Log.d("Response Data", "debug $e")
            }
        }
    }
}

7. Main 作成

MainActivity.kt

BuildConfig.API_KEYの部分に、”API Key”を入れる。

class MainActivity : ComponentActivity() {

    companion object{
        const val BASE_URL = "https://api.openweathermap.org"
        const val UNITS = "metric"
        const val LANG = "ja"

        const val APIKEY = BuildConfig.API_KEY //<-- API Keyをここに入れる
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        val viewModel = ViewModelProvider(this)[ForecastViewModel::class.java]
        super.onCreate(savedInstanceState)
        setContent {
            WeatherForecastJpTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize()
                ) {
                        mainScreen(viewModel)
                }
            }
        }
    }
}

MainScreen.kt

@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun mainScreen(viewModel:ForecastViewModel) {

    val city = remember { mutableStateOf("") }
    val weather = remember { mutableStateListOf<ForecastItems>() }
    val keyboardController = LocalSoftwareKeyboardController.current

   Box(modifier = Modifier.background(Color.LightGray)){
      Column {

         Row(modifier = Modifier
             .fillMaxWidth()
             .wrapContentHeight(),
             horizontalArrangement = Arrangement.SpaceEvenly,
             verticalAlignment = Alignment.CenterVertically
         ){
             var  name by remember { mutableStateOf("")}

                 OutlinedTextField(value = name, onValueChange = {name = it},
                     label = {Text(stringResource(id = R.string.city_name), color = Color.Blue)},
                     modifier = Modifier
                         .padding(10.dp),
                     colors = TextFieldDefaults
                         .outlinedTextFieldColors(textColor = Color.Blue,
                         focusedLabelColor = Color.Blue,
                             focusedBorderColor = Color.Blue)
                 )
                Column {

                    Spacer(Modifier.size(10.dp))

                    Button(modifier = Modifier,
                        onClick = {
                            runBlocking {
                                viewModel.getForecast(name, city, weather)
                            }
                            name = ""
                            keyboardController?.hide()
                        },
                        contentPadding = PaddingValues(8.dp),
                        colors = ButtonDefaults.textButtonColors(
                            Color.Blue,
                            contentColor = Color.White,
                            disabledContentColor = Color.LightGray),
                    ){
                        Text("GO")

                    }
                }

        }

          Text(city.value,
              modifier = Modifier
                  .fillMaxWidth()
                  .padding(start = 0.dp, top = 8.dp, end = 25.dp, bottom = 8.dp),
              textAlign = TextAlign.End,
              fontSize = 24.sp,
              color = Color.Blue
          )
          
          LazyColumn(modifier = Modifier
              .fillMaxSize()
              .padding(start = 25.dp, top = 5.dp, end = 25.dp, bottom = 5.dp)){
              items(weather){weather -> WeatherTtem(weather)}
          }
      }
     }
  }

@Composable
fun WeatherTtem(weather: ForecastItems) {
    Column {
        Box(modifier = Modifier
            .fillMaxWidth()
            .background(color = Color.Gray, shape = RoundedCornerShape(10.dp))
            .padding(8.dp)
           ){
            Column {
                Row(modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceBetween,
                    verticalAlignment = Alignment.CenterVertically
                    ) {
                    Text(text = weather.date, modifier = Modifier, textAlign = TextAlign.Start, fontSize = 20.sp, color = Color.White)
                    Text(text = weather.description, modifier = Modifier.padding(start = 0.dp, top = 0.dp, end = 15.dp, bottom = 0.dp)
                        ,textAlign = TextAlign.End ,fontSize = 20.sp, color = Color.White)
                }
                Row(modifier = Modifier
                    .fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceBetween,
                    verticalAlignment = Alignment.CenterVertically) {
                    Column {
                        Row(modifier = Modifier,
                            horizontalArrangement = Arrangement.SpaceBetween,
                            verticalAlignment = Alignment.CenterVertically){

                            Text(text =stringResource(id = R.string.temp),
                                modifier = Modifier
                                    .padding(start = 10.dp, top = 5.dp, end = 10.dp, bottom = 5.dp),
                                textAlign = TextAlign.Start,
                                fontSize = 18.sp, color = Color.White)

                            Text(text = weather.temp.toString(),
                                modifier = Modifier
                                    .padding(start = 0.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
                                textAlign = TextAlign.Center,
                                fontSize = 18.sp, color = Color.White)

                            Text(text = stringResource(id = R.string.temp_unit),
                                modifier = Modifier
                                    .padding(start = 5.dp, top = 8.dp, end = 0.dp, bottom = 5.dp),
                                textAlign = TextAlign.Start,
                                fontSize = 16.sp, color = Color.White)
                        }
                        Row(modifier = Modifier,
                            horizontalArrangement = Arrangement.SpaceBetween,
                            verticalAlignment = Alignment.CenterVertically) {

                            Text(text =stringResource(id = R.string.humidity),
                                modifier = Modifier
                                    .padding(start = 10.dp, top = 5.dp, end = 10.dp, bottom = 5.dp),
                                textAlign = TextAlign.Start,
                                fontSize = 18.sp, color = Color.White)

                            Text(text = weather.humidity.toString(),
                                modifier = Modifier
                                    .padding(start = 0.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
                                textAlign = TextAlign.Center,
                                fontSize = 18.sp, color = Color.White)

                            Text(text = stringResource(id = R.string.humidity_unit),
                                modifier = Modifier
                                    .padding(start = 5.dp, top = 8.dp, end = 0.dp, bottom = 5.dp),
                                textAlign = TextAlign.Start,
                                fontSize = 16.sp, color = Color.White)
                        }

                        Row(modifier = Modifier,
                            horizontalArrangement = Arrangement.SpaceBetween,
                            verticalAlignment = Alignment.CenterVertically) {

                            Text(text =stringResource(id = R.string.pressure),
                                modifier = Modifier
                                    .padding(start = 10.dp, top = 5.dp, end = 10.dp, bottom = 5.dp),
                                textAlign = TextAlign.Start,
                                fontSize = 18.sp, color = Color.White)

                            Text(text = weather.pressure.toString(),modifier = Modifier
                                .padding(start = 0.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
                                textAlign = TextAlign.Center,
                                fontSize = 18.sp, color = Color.White)

                            Text(text = stringResource(id = R.string.pressure_unit),
                                modifier = Modifier
                                    .padding(start = 5.dp, top = 8.dp, end = 0.dp, bottom = 5.dp),
                                textAlign = TextAlign.Start,
                                fontSize = 16.sp, color = Color.White)
                        }

                    }
                    val imageUrl = weather.icon
                    Image(
                        painter = rememberAsyncImagePainter(imageUrl),
                        contentDescription = null,
                        modifier = Modifier
                            .size(100.dp)
                            .padding(start = 0.dp, top = 0.dp, end = 20.dp, bottom = 0.dp)
                    )

                }
            }
        }
        Spacer(Modifier.size(20.dp))
    }
}

8. 出来上がり

コードはこちら: Kazu0721/WeatherForecastJp

デザイン、構成等まだまだ勉強します。

こんな感じです。