初心者の初心者による初心者のためのAndroidアプリ開発

#はじめに


最近はコロナウイルスのおかげで勉強がはかどっています笑　2回の投稿を終えてAndroidアプリ開発もそれなりに進んだ内容となってきました。そして、今回は既存のウェブサイトと連携したアプリを作成していきたいと思います。Androidアプリの方で都市を選択すると対応した現在の天気情報を取得して表示するという形式のものです。実用性のあるものなので頑張っていきましょう。

#AsyncTaskの作成
今回のアプリの初期状態では、次の画像のように都市のリストが上半分に表示され、下半分にウェブサイトから天気情報を取得して表示するようになっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552605/0ed3599e-f98b-fa6d-dfe0-2cbd4acbd26b.png" width="50%" >
文字列情報の追加とレイアウトファイルの編集は前回までの学習内容でできると思いますので頑張ってみてください。

res/values/strings.xml

~~~
<resources>
    <string name="app_name">
        全国の天気
    </string>
    <string name="tv_winfo_title">
        お天気情報
    </string>
</resources>
~~~
res/layout/activity_main.xml

~~~
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/lvCityList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.5"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="10dp"
        android:layout_weight="0.5"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:gravity="center"
            android:text="@string/tv_winfo_title"
            android:textSize="25sp"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvCityName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="20sp"/>

            <TextView
                android:id="@+id/tvWeatherTelop"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textSize="20sp"/>
        </LinearLayout>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/tvWeatherDesc"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textSize="15sp"/>
        </ScrollView>
    </LinearLayout>
</LinearLayout>
~~~

次にアクティビティ処理を記述していきます。

java/com.websarva.wings.android.a03_16_Asyncsample/MainActivity

~~~
package com.websarva.wings.android.a03_16_asyncsample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ListView
import android.widget.SimpleAdapter
import android.widget.TextView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val lvCityList = findViewById<ListView>(R.id.lvCityList)
        val cityList: MutableList<MutableMap<String, String>> = mutableListOf()

        var city = mutableMapOf("name" to  "東京", "id" to "2")
        cityList.add(city)

        city = mutableMapOf("name" to "大阪", "id" to "2")
        cityList.add(city)

        city = mutableMapOf("name" to "名古屋", "id" to "2")
        cityList.add(city)

        val from = arrayOf("name")
        val to = intArrayOf(android.R.id.text1)

        val adapter = SimpleAdapter(applicationContext, cityList, android.R.layout.simple_expandable_list_item_1, from, to)
        lvCityList.adapter = adapter
        lvCityList.onItemClickListener = ListItemClickListener()
    }

    private inner class ListItemClickListener : AdapterView.OnItemClickListener
    {
        override fun onItemClick(parent: AdapterView<*>, view: View?, position: Int, id: Long)
        {
            val item = parent.getItemAtPosition(position) as Map<String, String>
            val cityName = item["name"]
            val cityId = item["id"]

            val tvCityName = findViewById<TextView>(R.id.tvCityName)
            tvCityName.setText(cityName + "の天気: ")
        }
    }
}
~~~
今回使用しているウェブサイトのライブドアの天気情報の仕様は、**http://weather.livedoor.com/forecast/webservice/json/v1**というURLの後ろにパラメータとして**city=都市のID**を付与してGETリクエストを送信すると天気情報がJSONデータとして返ってくるものです。

サンプルで載せているコードは東京、大阪、名古屋の三都市のみですが、他の都市の情報を表示したい場合は次のサイトを参考にしてみてください。
https://www.kaden1000.com/2013/04/livedoor-weather-hacks1%E6%AC%A1%E7%B4%B0%E5%88%86%E5%8C%BA%EF%BC%88city%E3%82%BF%E3%82%B0%EF%BC%89%E3%81%AE%E5%9C%B0%E5%9F%9Fid%E4%B8%80%E8%A6%A7%E3%81%BE%E3%81%A8%E3%82%81/

#非同期処理コードの記述
はじめに、AsyncTaskを使うためのAsyncTaskを継承したメンバクラスを作成していきます。まずはメンバクラスの作成のみで、実際にインターネットに接続して天気情報サービスからデータを取得するコードと取得したJSON文字列を解析するコードは後半で追記していきます。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

~~~
            val tvCityName = findViewById<TextView>(R.id.tvCityName)
            tvCityName.setText(cityName + "の天気: ")
        }
    }

　　　　　　　　〜以降追記〜
    private inner class WeatherInfoReceiver(): AsyncTask<String, String, String>()
    {
        override fun doInBackground(vararg params: String?): String
        {
            val id = params[0]
            val urlStr = "http://weather.livedoor.com/forecast/webservice/json/v1?city=${id}"
            return result
        }

        override fun onPostExecute(result: String?)
        {
            val tvWeatherTelop = findViewById<TextView>(R.id.tvWeatherTelop)
            val tvWeatherDesc = findViewById<TextView>(R.id.tvWeatherDesc)
            tvWeatherTelop.text = telop
            tvWeatherDesc.text = desc
        }
    }
}
~~~

次に、先ほど作成したWeatherInfoReceiverを実行する処理を記述していきます。リストがタップされたときに実行する処理なのでListItemClickListenerメンバクラスのonItemClick()メソッドの末尾にコードを追記していきます。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

~~~
            val tvCityName = findViewById<TextView>(R.id.tvCityName)
            tvCityName.setText(cityName + "の天気: ")

            〜以降追記〜
            val receiver = WeatherInfoReceiver()
            receiver.execute(cityId)
        }
    }
~~~

#HTTP接続の実装
非同期処理の準備ができたのでインターネットに接続して天気情報を取得していきたいと思います。
インターネットに接続して天気情報を取得する処理はバックグラウンドで行うためdoInBackground()に記述します。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

~~~
            val id = params[0]
            val urlStr = "http://weather.livedoor.com/forecast/webservice/json/v1?city=${id}"

            〜以降追記〜
            val url = URL(urlStr)
            val con = url.openConnection() as HttpURLConnection

            con.requestMethod = "GET"
            con.connect()

            val stream = con.inputStream
            val result = is2String(stream)
            
            con.disconnect()
            stream.close()
~~~

次に、InputStreamオブジェクトを文字列に変換するprivateメソッドをWeatherInfoReceiverクラスに追記します。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

~~~
            tvWeatherTelop.text = telop
            tvWeatherDesc.text = desc
        }
    }

            〜以降追記〜    
    private fun is2String(stream: InputStream): String
    {
        val sb = StringBuilder()
        val reader = BufferedReader(InputStreamReader(stream, "UTF-8"))
        var line = reader.readLine()
        
        while (line != null)
        {
            sb.append(line)
            line = reader.readLine()
        }
        
        reader.close()
        return sb.toString()
    }
}
~~~

また、doInBackground()で取得したJSON文字列を解析するために、onPostExcute()に処理を記述します。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

~~~
        override fun onPostExecute(result: String?)
        {

            〜以降追記〜
            val rootJSON = JSONObject(result)
            val descriptionJSON = rootJSON.getJSONObject("description")
            val desc = descriptionJSON.getString("text")
            val forecasts = rootJSON.getJSONArray("forecasts")
            val forecastNow = forecasts.getJSONObject(0)
            val telop = forecastNow.getString("telop")
~~~

最後に、AndroidManifestにタグと属性を追記しましょう。
Androidではアプリがインターネットと接続するには、ぞの許可をアプリに与える必要があります。そのため、AndroidManifest.xmlに次の2つのタグとapplicationのタグ内に属性を追記します。

manifests/AndroidManifest.xml

~~~
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.websarva.wings.android.a03_16_asyncsample">

            〜以降追記〜
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
~~~

では、アプリを起動していきましょう！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552605/65604bd9-a1bc-b17f-5b23-1d5fae38d8bb.png" width="50%">
**完成です！**

#最後に
今回はコードを書く量が多めだったので諦めそうになった方もいるかもしれませんが、完成品を見ると頑張ったなと思えるかもしれません。実用性のあるアプリなので使ってみてはいかがでしょうか。

本記事は、 『基礎＆応用力をしっかり育成！Androidアプリ開発の教科書Kotlin対応なんちゃって開発者にならないための実践ハンズオン』を参照にしております。