概要
- UI処理(メインスレッド、大体はMainActivity)とHTTPリクエストはスレッドを分けないといけない。そのため、AsyncTaskクラスを継承した自作クラスを作り、doInBackgroundメソッドをオーバーライドして、そこにHTTPリクエスト部分を書く。
- 以下の処理は例外がいろいろ返ってくるかもしれないので、try{}の中に書く。
- URLオブジェクトを作成。今回はGETメソッドで、パラメーターを付け足していくので、文字列のURIエンコードもする。
- URLオブジェクトのopenConnection()メソッドでURLConnectionインスタンスをHttpURLConnectionとして取得し、connect()メソッドでリモートリソースに接続、データ取得。
- 大体JSON形式で返ってくる(XMLもあり)ので、取得したデータを文字列として引数に渡したJSONObjectを生成してデータを取り出す。
- 返ってくる例外はMalformedURLException、IOException、JSONExceptionの3つ。
- 最後にHttpURLConnectionインスタンスのdisconnectメソッドで接続を切る。取得したデータを溜めているバッファーも閉じるけど、例外出るかも。
完成コード
以下全てKotlin。
AsyncHttpRequest.kt
import android.os.AsyncTask import java.io.BufferedReader import java.io.IOException import java.io.InputStream import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.URL class AsyncHttpRequest() : AsyncTask<String, Void, String>() { override fun doInBackground(vararg urls: String?): String? { // The argument will be encoded URL strings var connection: HttpURLConnection? = null var reader: BufferedReader? val buffer: StringBuffer try { val url = URL(urls[0]) connection = url.openConnection() as HttpURLConnection connection.connect() val statusCode: Int = connection.responseCode if (statusCode != HttpURLConnection.HTTP_OK) { System.err.println("Connection faild. statusCode: " + statusCode) return null } val stream = connection.inputStream val inReader = InputStreamReader(stream) reader = BufferedReader(inReader) buffer = StringBuffer() var line: String? while (true) { line = reader.readLine() if (line == null) { break } buffer.append(line) } return buffer.toString() } catch (e: IOException) { e.printStackTrace() } finally { connection?.disconnect() try { reader?.close() } catch (e: IOException) { e.printStackTrace() } } return null } }
MainActivity.kt
class MainActivity : AppCompatActivity() { (省略) private fun getTranslationFromGoogle (originalText : String) : String { val myURLForGoogle : String = "https://translation.googleapis.com/language/translate/v2?target=en&q=" + Uri.encode(originalText) val myResult : String? = AsyncHttpRequest().execute(myURLForGoogle).get() if (myResult != null) { try { val parentJsonObj = JSONObject(myResult) val myTranslatedText : String = parentJsonObj.getJSONObject("data") .getJSONArray("translations") .getJSONObject(0) .getString("translatedText") return myTranslatedText } catch (e: JSONException) { e.printStackTrace() return "Failed persing JSON." } } else { return "Translation failed." } } }
AsyncTaskクラス
AsyncHttpRequest.kt
というファイルを追加して
class AsyncHttpRequest() : AsyncTask<T1, T2, T3>() { override fun doInBackground(vararg params: T1?): T3 { (処理) return T3型の何か } }
と書く。(要import
)
呼び出したいときはメインスレッドで
AsyncHttpRequest().execute(T1型の引数) // 実行されるだけで返り値は返ってこない AsyncHttpRequest().execute(T1型の引数).get() // 返り値(T3型)が欲しいとき
とする。
解説
T1
・・・execute
メソッドの引数の型を指定T2
・・・ 途中経過を発行するメソッドの戻り値の型を指定(後述)T3
・・・.execute(T1).get()
の戻り値の型を指定
総称型なので、自分で決められる。例えば
class AsyncHttpRequest() : AsyncTask<String, Void, String>() { override fun doInBackground(vararg urls: String?): String? { (処理) return String型の何か または null } }
こんな感じ。
プログレスバーなんかで途中経過を表示したいときはT2
にInt
か何かを指定する。今回は途中経過は使わないので、知りたい人は「doInBackground
メソッドの中でpublishProgress
メソッドを実行するとonProgressUpdate
メソッド(要override)が呼ばれる」という方向性で検索してね。
また、T3
型の返り値を返すだけじゃなく、その返り値を使ってちょこちょこ処理を続けたいときはonPostExecute
メソッドをオーバーライドしてその中でちょこちょこ処理を続ける。これはメインスレッドで実行される。
また、doInBackground
内でUIに変更を加えないようにしないといけない。UIの変更(例えばテキストビューに文字列をセットするとか)はメインスレッド上で行うので、バックグラウンドからちょっかいを出してはいけない。なぜなのかは参考サイトを参考に。どうしてもちょっかいを出したい場合はonProgressUpdate
かonPostExecute
の中で行う。
参考
AsyncTask | Android Developers
[Android] 非同期処理 AsyncTaskの使い方
android.os.AsyncTaskの正しい使い方 – 株式会社ライトコード
URLオブジェクトとURIエンコードとURLConnectionインスタンス
まずURLオブジェクトから。こいつは後述するURLConnectionインスタンスを生成するのに必要。
val url = URL(URLを文字列として代入)
ここで、URLにスペースや日本語を付加しないといけない場合、エンコードが必要。
val urlString = "https://translation.googleapis.com/language/translate/v2?q=" + Uri.encode("こんにちは") // こんにちは → %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF val url = URL(urlString)
もしこのとき、URL文字列がイミフだった場合URLオブジェクトが作れず、例外MalformedURLException
が発生する。これはIOException
を継承したものなので、まとめてキャッチできるのかな?よく分からん。でも今回は起こり得なさそうなので放置。
で、生成したURLオブジェクトのopenConnection
メソッドでURLConnectionインスタンスを、HttpURLConnectionインスタンスとして作る。接続への道を開く感じ?
var connection : HttpURLConnection? = null try { connection = url.openConnection() as HttpURLConnection
失敗すると例外IOException
が発生すると思われる。なのでtry-catch構文の中に書くこと。
HttpURLConnectionインスタンスを作ったタイミングでヘッダーやメソッドなどがセットできる。(任意)
// リクエストメソッド connection.setRequestMethod("GET") connection.setRequestMethod("POST") // POSTメソッドの場合、これをしないとボディが使えない connection.setDoOutput(true) // タイムアウトの設定 connection.setConnectTimeout(100000) connection.setReadTimeout(100000) // ヘッダー connection.setRequestProperty("User-Agent", "Android") connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8") // リダイレクト設定 connection.setInstanceFollowRedirects(false) // リダイレクトしない
タイムアウトについて。setConnectionTimeout
とsetReadTimeout
で設定した時間を過ぎても接続が完了しなかったりデータ読み込みが完了しなかった場合、例外SocketTimeoutException
が発生するって公式ドキュメントのじっちゃが言ってた。
そして接続。
connection.connect()
もしサーバーからエラーコード4xxとか5xxとか返ってきたらどうしよう。一応エラーコードが読み取れるなら以下の処理で対処可能
val statusCode: Int = connection.responseCode if (statusCode != HttpURLConnection.HTTP_OK) { System.err.println("Connection faild. statusCode: " + statusCode) (return nullとか途中で処理を終了するようなコード) }
でもエラーコードが返ってきた時点で、FileNotFoundException
なる例外が返る可能性があるみたいで、しかもそれはIOException
を継承したものなので、IOException
として処理されてしまうかもしれない。うーん、知らん。
受信データの読み込みは次の節で解説するとして、いろいろ処理が終わったら接続を閉じる。
} catch (e: IOException) { e.printStackTrace() } finally { connection?.disconnect() }
URLオブジェクトのopenConnection
メソッドで例外が発生した場合、変数connection
は初期値null
のまま放置されているはずなので、?.
にすること。
参考
URL | Android Developers
Uri | Android Developers
URLを表す文字列を指定してURLオブジェクトを作成する – URLクラス – Swing
URLConnection(HttpURLConnection)と向き合おう~GETメソッドでデータを取得する~
URLConnection | Android Developers
HttpUrlConnection とエラーハンドリング – Qiita
HttpURLConnectionで嵌った話 | KATSUMI KOKUZAWA’S BLOG
InputStreamについて
無事接続が正しく行われたら、次はデータ読み込み。
val stream = connection.inputStream
これで、変数stream
にサーバーからのレスポンスを読み取るためのInputStream
インスタンスがセットされる。
なんじゃそりゃと思わない人は読み飛ばし推奨。
Javaでは外部データをStreamという概念で扱う。InputStream
インスタンスが生成された時点でそれは「外部からアプリ内に入ってくる(Input) – データ(Stream)」を保持しているものと見なせる。オブジェクトの形をとっているけど、要はデータそのもの。
こいつに対して、一応データ読み出しをさせることはできるけどbyte単位。
stream.read() // 123 stream.read() // 10 stream.read() // 32 stream.read() // 32
read
メソッドを繰り返すと勝手に次々読んでくれる。byte単位だけど。しかも出力は整数型。
なので、普通はリーダー(Reader)となるInputStreamReader
を被せてやる。
val inReader = InputStreamReader(stream) val inReader = InputStreamReader(stream, "UTF-8") // 文字コードを指定するとき
これで文字ごとに読んでいくんだけど・・・
inReader.read() // 123 inReader.read() // 10 inReader.read() // 32 inReader.read() // 32
データが1byte文字(つまり半角)で書かれていたら結局一緒になる。もしデータが2byte文字(つまり全角)で書かれていたら2byteまとめて出力する。
ただ、こんなことは普通しないで、実際は一度にがばっと読み込んでメモリに溜めといてから必要な部分を取り出す。その役目をするのがBufferedReader
クラス。こいつでInputStreamReader
を包んでやる。
val reader = BufferedReader(inReader)
ここまでくると、readLine
メソッドが使えるようになる。以下は一行ずつ読み込む処理。
var buffer: String var line: String? while (true) { line = reader.readLine() if (line == null) { break } buffer += line }
ただ、これをやるとマズイ。というのもString
型の変数は基本的に不変、つまり一度セットした値を変えることはできないので、buffer
に変更があるたびに実は裏で新しい変数が作られる(ちょっと説明を端折ったけど大体そんな感じ)。もちろん変更前のものもそのまま放置。これだとメモリを食っちゃう(数万回やれば)。
どうするのかと言うと、StringBuffer
クラスを使う。これは可変長なString
型と見なせる。最後に文字列を付けたしたい場合はappend
メソッド、途中に文字列を追加したい場合はinsert
メソッドを使う。
val buffer = StringBuffer() var line: String? while (true) { line = reader.readLine() if (line == null) { break } buffer.append(line) }
ちなみに同様の働きをするStringBuilder
というクラスもあって、StringBuffer
と同じメソッドで文字列を追加していく。違いはStringBuilder
はシングルスレッド向き、StringBuffer
はマルチスレッド向きだそう。
今回はどちらでも良さそうな感じだけど、もしスレッドをまたいで参照される場合はStringBuffer
の方がいいのかな?
読み込みが終わったらBufferedReader
を閉じる。一番外側だけ閉じれば中も閉じられるみたい。
でもせっかくならfinally
の中に書いて確実に閉じられるようにする。例外が発生するかもだし。多分しないけど。
finally { connection?.disconnect() try { reader?.close() } catch (e: IOException) { e.printStackTrace() } }
最後にdoInBackground
メソッドの返り値として読み取ったデータを返しましょう。
return buffer.toString()
参考
ストリーム
InputStream | Android Developers
InputStreamReader | Android Developers
BufferedReader | Android Developers
【Java言語】StringBuilderの使い方とメリット | 侍エンジニア塾ブログ(Samurai Blog) – プログラミング入門者向けサイト
【5分で分かる】Java言語のStringBufferの使い方とStringBuilderとの違い | 侍エンジニア塾ブログ(Samurai Blog) – プログラミング入門者向けサイト
StringBuffer | Android Developers
Java:Reader/Writerにおけるclose()メソッド呼び出しの流儀: 愛ゆえにプログラムは美しい
JSONオブジェクト
やっとこさ読み込んだデータをゲット。中身は以下のJSON形式。
{ "data": { "translations": [ { "translatedText": "Hallo Welt", "detectedSourceLanguage": "en" } ] } }
{ }
で囲まれているものがオブジェクト、[ ]
で囲まれているものは配列として取り出せる。
上記の内容が変数myResult
に文字列として格納されているとする。
val parentJsonObj = JSONObject(myResult)
これで、上のJSONデータを丸ごとオブジェクトとして変数parentJsonObj
に格納する。その後、data
と名前が付いたJSONオブジェクトを取り出すので、
val myData = parentJsonObj.getJSONObject("data")
次にtranslations
と名前の付いた配列を取り出すので
val myTranslations = myData.getJSONArray("translations")
その配列の中にはJSONオブジェクトが一つ格納されているので、1番目の要素を取り出すということで、
val myTranslation = myTranslations.getJSONObject(0)
さらに、そのJSONオブジェクトの中のtranslatedText
という名前の付いた文字列を取り出すので、
val myTranslatedText : String = myTranslation.getString("translatedText")
これを一気に書くと
val parentJsonObj = JSONObject(myResult) val myTranslatedText : String = parentJsonObj.getJSONObject("data") .getJSONArray("translations") .getJSONObject(0) .getString("translatedText")
最後に、JSONオブジェクトや配列の取り出しに失敗するとJSONException
という例外が発生するので、try-catchの中に書く。
try { val parentJsonObj = JSONObject(myResult) val myTranslatedText : String = parentJsonObj.getJSONObject("data") .getJSONArray("translations") .getJSONObject(0) .getString("translatedText") } catch (e: JSONException) { e.printStackTrace() }
成し遂げたぜ。
参考
JSONで配列の入れ子構造や値の取得方法などをPythonを使って説明! | 侍エンジニア塾ブログ(Samurai Blog) – プログラミング入門者向けサイト
JSONObject | Android Developers
JSONArray | Android Developers
【Android(Java)】JSONデータをパースする方法 – TechBlog
AndroidでJSONの読み書き – Ararami Studio
全体の参照ページ
AndroidからAPIを叩いてJSON取って中身を表示させるまで – Qiita
Android Studioで天気情報を表示するアプリを作ってみた – RAKUS Developers Blog | ラクス エンジニアブログ