【Androidアプリ(java)】シンプルな天気予報アプリを作る(非同期処理)

どうもこんにちは、
Androidアプリ(java)で簡単な天気予報アプリを作ってみました。
天気情報は、以下の天気予報APIを使います。
天気予報API
完成形のソースは、Githubにアップしました。
Github(weather_simple)
動画解説
動画解説もあります。記事と合わせてどうぞ!
今回作るアプリ
画面上部に、都市を選択するスピナーを配置します。
都市を選択し、「今日の天気は?」ボタンを押すと、その都市の今日の天気を表示する簡単なアプリです。
レイアウト
まずは、画面のレイアウトを実装します。
activity_main.xmlを以下のように実装します。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<!-- スピナー -->
<Spinner
android:id="@+id/city_spinner"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_gravity="center"/>
<!-- 選択地 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:layout_gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="選択地:"/>
<TextView
android:id="@+id/select_city_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""/>
</LinearLayout>
<!-- 今日の天気は?ボタン-->
<Button
android:id="@+id/weather_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_gravity="center"
android:text="今日の天気は?"/>
<!-- telop text -->
<TextView
android:id="@+id/telop_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:textSize="20sp"
/>
<!-- description text -->
<ScrollView
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
>
<TextView
android:id="@+id/description_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</ScrollView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
都市選択スピナーを実装する
まず初めに、都市選択スピーナーを実装します。
今回使う天気予報APIでは都市ごとにIDが割り振られており、URLの末尾にIDを指定することで都市の天気情報を取得することができます。
都市のIDは、以下から確認できます。
天気予報API(都市ID)
例:以下福岡市の天気(400010が福岡市のID)
https://weather.tsukumijima.net/api/forecast/city/400010
ではMainActivityにスピナー処理を追加していきましょう。
まず、都市のIDを管理するため、HashMapで都市名をキーにしてIDを追加します。
※とりあえず、福岡〜熊本を追加しましたが、好きな都市を自由に追加してください。
public class MainActivity extends AppCompatActivity {
private final HashMap<String, String> city_map = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 都市選択スピナー
// 都市とIDを設定
city_map.put("福岡","400010");
city_map.put("佐賀","410010");
city_map.put("長崎","420010");
city_map.put("熊本","430010");
次にスピナーの設定です。
HashMapのcity_mapからキーの配列を取得し、citiesに設定します。
あとは、adapterを作ってスピナーにセットしてあげます。
// 都市選択スピナー
// 都市とIDを設定
city_map.put("福岡","400010");
city_map.put("佐賀","410010");
city_map.put("長崎","420010");
city_map.put("熊本","430010");
String[] cities = city_map.keySet().toArray(new String[0]);
Spinner city_spinner = findViewById(R.id.city_spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,cities);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
city_spinner.setAdapter(adapter);
スピナーでアイテムが選択されたときのリスナーを実装します。
アイテムが選択されたら、その都市名を選択地:横のtextViewに設定します。
// 都市選択スピナー
// 都市とIDを設定
city_map.put("福岡","400010");
city_map.put("佐賀","410010");
city_map.put("長崎","420010");
city_map.put("熊本","430010");
String[] cities = city_map.keySet().toArray(new String[0]);
Spinner city_spinner = findViewById(R.id.city_spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,cities);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
city_spinner.setAdapter(adapter);
city_spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
String selectedCity = cities[i];
TextView textView = findViewById(R.id.select_city_text);
textView.setText(selectedCity);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
非同期処理で天気予報情報取得処理を実装する
普段描画などを処理しているUIスレッドで天気予報APIを使った通信処理を行うと、描画処理が待たされて問題が発生します。

従って、通信処理などの時間がかかる処理は、UIスレッドではなく、別のバックグラウンドスレッドなどで実行する必要があります。これを非同期処理といいます。

バックグラウンド処理を実装するには、まずRunnnableインタフェースを実装したクラスを作り、ExecutorServiceで実行します。
今回は、Runnnableを実装した”WeatherInfoRecevier”というクラスを作成しました。
・Runnnable:Javaインタフェース バックグラウンドで処理させたいことをrun()に実装する
・ExecutorService:スレッド管理クラス バックグラウンド処理でタスク制御するときに使う
以下のように、”WeatherInfoRecevier”という新規Java Classを作成します。
public class WeatherInfoReceiver implements Runnable{
final String TAG = "WeatherInfoReceiver";
Handler handler = new Handler(Looper.getMainLooper());
private String result;
private final String city_id;
private final TextView telop_text;
private final TextView description_text;
public WeatherInfoReceiver(String id, TextView text1, TextView text2){
city_id = id;
telop_text = text1;
description_text = text2;
}
@Override
public void run(){
// バックグラウンド処理
Log.i(TAG, "run() - Current Thread: " + Thread.currentThread().getName());
result = doInBackground();
// 処理終了(UIスレッドに結果を渡す)
handler.post(new Runnable() {
@Override
public void run() {
Log.i(TAG, "run() - Current Thread: " + Thread.currentThread().getName());
onPostExecute(result);
}
});
}
private String doInBackground() {
Log.i("debug","doInBackground city_id="+city_id);
String result="";
String urlStr = "https://weather.tsukumijima.net/api/forecast/city/"+city_id;
HttpURLConnection con = null;
InputStream is = null;
try {
URL url = new URL(urlStr);
con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.connect();
is = con.getInputStream();
result = is2String(is);
Log.i("debug","doInBackground");
}
catch (IOException ex){
Log.e("debug",ex.toString());
}
finally {
if(con != null){
con.disconnect();
}
if(is != null){
try {
is.close();
}
catch (IOException ex){
Log.e("debug",ex.toString());
}
}
}
return result;
}
private void onPostExecute(String result) {
// 処理終了時の処理
Log.i(TAG, "onPostExecute() - Result: " + result);
String telop = "";
String desc = "";
try{
JSONObject jsonObject = new JSONObject(result);
JSONObject jsonDescription = jsonObject.getJSONObject("description");
desc = jsonDescription.getString("text");
JSONArray jsonArray = jsonObject.getJSONArray("forecasts");
JSONObject jsonForecastsNow = jsonArray.getJSONObject(0);
telop = jsonForecastsNow.getString("telop");
}
catch (JSONException ex){
Log.e("debug",ex.toString());
}
// JSONから取得した結果を画面に表示
telop_text.setText(telop);
description_text.setText(desc);
}
private String is2String(InputStream is) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuffer sb = new StringBuffer();
char[] b = new char[1024];
int line;
while(0 <= (line = reader.read(b))) {
sb.append(b, 0, line);
}
return sb.toString();
}
}
コンストラクタでは、天気予報APIで必要な都市ID、結果を表示するためのtextView(telopとdescription)を引数で設定します。
run()メソッド内にバックグラウンド処理を記載します。
doInBackground()メソッド内で天気予報APIのURLへアクセスし、情報を取得します。
取得結果はStringで返します。
最終的に、取得した結果を画面に描画したいので、バックグラウンド処理からUIスレッドに結果を渡します。
handlerを使って、postすれば、メッセージがキューイングされUIスレッドで処理が実行されます。
具体的には、onPostExecute()メソッドがUIスレッドで行う結果表示処理です。
onPostExecuteでは、天気予報APIから取得した結果のjson形式を解析し、JSONからtelop情報とdescription情報を取得しtextViewに設定しています。
作成したWeatherInfoReceiverクラスは、MainActivity側からボタンを押したときに、ExecutorServiceを使って実行します。
ボタン処理を実装する
MainActivityに、「今日の天気は?」ボタンの処理を実装します。
onCreate内に以下を実装しましょう。
// 今日の天気は? ボタン
findViewById(R.id.weather_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
TextView textView = findViewById(R.id.select_city_text);
String select_city = textView.getText().toString();
if(!select_city.isEmpty()){
String city_id = city_map.get(select_city);
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new WeatherInfoReceiver(city_id,findViewById(R.id.telop_text),findViewById(R.id.description_text)));
}
}
});
ボタンが押されたら、選択している都市名を取得し、city_mapから都市名に関連するIDを取得します。(9行目)
ExecutorServiceのsubmitメソッドで、WeatherInfoReceiverのインスタンスを指定しバックグラウンド処理を実行します。(10〜11行目)
マニフェストに権限を追加する
WebAPIを使うため、Androidのマニフェストファイルにインターネット接続に関する権限を追加します。
AndroidManifest.xmlに以下を追記します。
<?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"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
実行してみる
一通り実装が終わったら、アプリをインストールして実行してみます。
以下のように各都市の今日の天気が取得できました!

最後に
Androidアプリに通信機能など搭載していくと必ず非同期処理が必要になってきます。
シンプルな天気予報アプリで非同期処理の実装にチャレンジしてみてください。
それでは!


