【Android App】REST APIの呼び出し

概要

  • 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&amp;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
        
    }

}

こんな感じ。

プログレスバーなんかで途中経過を表示したいときはT2Intか何かを指定する。今回は途中経過は使わないので、知りたい人は「doInBackgroundメソッドの中でpublishProgressメソッドを実行するとonProgressUpdateメソッド(要override)が呼ばれる」という方向性で検索してね。

また、T3型の返り値を返すだけじゃなく、その返り値を使ってちょこちょこ処理を続けたいときはonPostExecuteメソッドをオーバーライドしてその中でちょこちょこ処理を続ける。これはメインスレッドで実行される。

また、doInBackground内でUIに変更を加えないようにしないといけない。UIの変更(例えばテキストビューに文字列をセットするとか)はメインスレッド上で行うので、バックグラウンドからちょっかいを出してはいけない。なぜなのかは参考サイトを参考に。どうしてもちょっかいを出したい場合はonProgressUpdateonPostExecuteの中で行う。

参考
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) // リダイレクトしない

タイムアウトについて。setConnectionTimeoutsetReadTimeoutで設定した時間を過ぎても接続が完了しなかったりデータ読み込みが完了しなかった場合、例外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 | ラクス エンジニアブログ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です