2022/08/07

[android] ネットワーク通信のライブラリがいろいろある

Linux でネットワーク通信を行うならば、クライアント側だと

  1. socket つくる
  2. サーバ側と connect する
  3. あとはよしなにー

という感じだったと思う。

ただまあ、socket のような素に近いところを直接アプリで触らせたくないと思う。ラップするならそこだろう。
なので「Android では通信するのに volley というライブラリを使う」と書いてあったので、うまいことラップされているのかな、くらいに思っていた。

しかしそこにあったのは、socket をラップしたライブラリとかではなく、なんだかいろいろ隠したライブラリだった。
最終的には「便利」なのかもしれないが、ちょっと通信したいのに使うには難しそうだった。
まあ、使わないことにはわからないので、基本的なところはやってみよう。

関係ない話だが、volley を検索して見ていたページが google.cn の方だった。偽サイトに引っかかったのか?とあせったが google.cn も google の証明書だった。よかった。


まず、最初に書かれているこちら。

Volley は、Android アプリのネットワーク操作を容易化、高速化する HTTP ライブラリです。

socket 通信ではなく、HTTP系専用らしい。
HTTP だったら、というわけでもないが、ネットワークに関しては普通の Java でもライブラリがあったはずだ。 volley だって Android の最初からあったわけではなく、どちらかといえばつい最近だったと思う。それまで HTTP で通信できなかったわけではないので、 volley でなくてもよいはずなのだ。

しかし、Android Developer の接続に関するページを見ると、2項目目にはもう volley が出てきているので、なんらかの理由で推しているのだろう。

接続  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/topics/connectivity?hl=ja

ただねぇ、ちょっと内側に行くと Retrofit というライブラリを使う説明をしてあった。なんなんだ。。。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting?hl=ja#choose-http-client

どうやらライブラリについていろいろと経緯があったらしい。

非同期通信をVolleyとOkHttpとRetrofitそれぞれで書いた場合の超簡易サンプル - Qiita
https://qiita.com/yamacraft/items/2bbc8867ef0a8ffb0945

ああ、okhttp も聞いたことがある。 gRPC を動かすときも okhttp のライブラリがなくてエラーになったし。
しかしすべてを把握したいわけでもない初心者としては、廃れなさそうなライブラリを 1つだけ覚えたいのだ。
volley はgoogle が開発しているから無難なのかな?
しかし OkHttp は HTTP/2 のことが書いてあったり、Retrofit と同じ Square が開発しているというのもあったりして魅力を感じてしまう

Overview - OkHttp
https://square.github.io/okhttp/

Retrofit は OkHttp を使っているので、HTTP 向けにしたラッパみたいなものなのかな? いや、 HTTP client だから違うな。 GitHub の About では、

OkHttp
Square’s meticulous HTTP client for the JVM, Android, and GraalVM

Retrofit
A type-safe HTTP client for Android and the JVM

となっているから、Retrofit はタイプセーフなところが売りなのだろう。

タイプセーフってなんじゃ?と思ったが、文字列ではなく Java や Kotlin の型で扱えることを指すのだろう。 volley はサンプルを見た感じでは文字列が返ってくるようだ。ただ、レスポンスが文字列、画像、JSON の場合は通常の Request で処理してくれるらしいし、特殊なことがしたければカスタマイズもできるようだ。画像って base64 なのかな?

どうせ私が使うなら JSON でやりとりするだろうし、それだったらどっちでもよいのかな。となると決め手がない。。。
なにか明確な違いがあればよかったのだが。
なので、どっちも同じことができるなら、実装してみて気に入った方を使うようにすれば良いと考えることにした。


ライブラリを試す前にちょっと脇道に逸れる。

新規でプロジェクトを作ろうとしたのだが、こっちもいろいろある。。。
どれが「普通」なのだろう?

image

まず、(Material3) の付いているのと付いていないのがある。
今のマテリアルデザインは 3 らしいので、付いていないのは 2 ってことか? 4 のページはなかったのでそうだと思う。

あとは、ただの Activity か Compose Activity かだ。
たぶん Jetpack Compose のことだろうと思うが、私に分かるんだろうか?

とりあえず Net1 という名前で作ってみた。
最初の 3行くらいは Kotlin の class の書き方なので分かるが、setContent 以降が初めて見る形だ。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    Net1Theme {
        Greeting("Android")
    }
}

Kotlin の文法をある程度把握する方が先かもしれんな・・・。


まず、Project Settings の Dependencies で "com.android.volley:volley" を search して追加。
今の時点では 1.2.1 が見つかった。

スニペットを貼り付けてみた。
Alt + Enter で解決させていったのだが、そもそもスニペットを Compose Activity の構造としてどこに置いたらいいのかがよくわからない。
@Compose の関数内だとコンテキストがダメそうだったので MainActivity の private 関数として追加し、 onCreate() から呼ぶようにした。画面に出すのはわからないから Log にした。

いつものように Android Studio の再生ボタンを押したのだが、画面は出るものの関数が呼ばれている気配がない。ブレークポイントも止まらない。
どうやら Compose Activity でプロジェクトを作った際には Configuration が DefaultPreview になっているようで、それだと MainActivity の onCreate() すら呼ばれないようだった。

Configuration を app に切り替えると、ようやく動きがあった。

E/Volley: [37] NetworkDispatcher.processRequest: Unhandled exception java.lang.SecurityException: Permission denied (missing INTERNET permission?)
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)

ああ、AndroidManifest.xml を追加していなかった。

<uses-permission android:name="android.permission.INTERNET" />

これだけで動いた。
Koglin で変更したのは MainActivity だけで済んだ。青文字がそれである。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        network()
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }

    private fun network() {
        // Instantiate the cache
        val cache = DiskBasedCache(cacheDir, 1024 * 1024) // 1MB cap

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.google.com"

        // Formulate the request and handle the response.
        val stringRequest = StringRequest(
            Request.Method.GET, url,
            { response ->
                // Do something with the response
                Log.d("network", response)
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }
}

呼び出しが一回だけだったら Volley.newRequestQueue() でよいそうだ。キャッシュを確保するのにもコストがかかるので、そこら辺は自分のアプリと相談することになるだろう。

JSON での API 実行のようにデータが小さいことがわかっている場合は、むしろキャッシュ無しという選択もあるんじゃなかろうか?
ああ、ちゃんと NoCache.java という

https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json

cache のところを書き換えても動いたので、それでよいのかな?
ついでに JSON のデータを取ってくるようにした。返ってくるデータが JSON の Array なのか Object なのかによって関数名が変わるのは注意しておきたいところだ。

    private fun network() {
        // Instantiate the cache
        val cache = NoCache()

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json"

        // Formulate the request and handle the response.
        val stringRequest = JsonArrayRequest(
            Request.Method.GET, url, null,
            { response ->
                // Do something with the response
                Log.d("network", response.toString())
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }

最初は String で受け取っていたのだけど、日本語の部分が文字化けするのだった。 JSON で取ると化けなかったのだけど何でだろう? Windows だからか?


volley の欠片くらいは味わえたと思うので、次は Retrofit だ。

https://square.github.io/retrofit/

こういう感じかな?

  1. Retrofilt.Builder() でアクセスする根っこを指定する
  2. interface を作って、 GET したりする下側を準備する
  3. Call で呼び出す

ここだと、根っこの部分が「https://api.github.com/」で、GET するのは「https://api.github.com/users/{ユーザ名}/repos」。
ユーザ名は "octocat" を指定している。

https://api.github.com/users/octocat/repos

 

まず Project Structure > Dependencies からライブラリを追加。
これを書いている時点では 2.9.0 だった。

com.squareup.retrofit2:retrofit

INTERNET の permission は既に追加してあるので、あとはまねしていけばよいだけ。

だとおもったのだが、「android.os.NetworkOnMainThreadException」が出てしまった。
コルーチンでやるのが Compose Activity っぽい気はするが、別スレッドにすればよかろうということで Tread で囲んだ。

    interface Tenki {
        @GET("/bosai/forecast/data/forecast/{area}.json")
        fun listTenkis(@Path("area") area: String?): Call<List<TenkiData?>?>?
    }

    class TenkiData(val publishingOffice: String, val reportDatetime: String)

    private fun networkRetrofit() {
        Thread {
            val retrofit = Retrofit.Builder()
                .baseUrl("https://www.jma.go.jp/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

            val service = retrofit.create(Tenki::class.java)
            val data = service.listTenkis("400000")
            val result: List<TenkiData?>? = data?.execute()?.body()
            Log.d("network", "${result?.get(0)?.publishingOffice}, ${result?.get(0)?.reportDatetime}")
        }.start()
    }

"?" が多いのは Android Studio の指摘をそのまま採用したためだ。
ここで TenkiData に取得しないパラメータを追加しても、それは別にエラーとはならなかった。なので、あれば読み取るし無ければ読み取らないだけのよう。型が違う場合には変換動作によって例外が発生する。


こうやってみると、楽かどうかという意味では volley の方が事前準備が少なくて楽だった。ただ、読み取って使うくせに型も知らないのか?という意味では Retrofit の方がよいのかもしれんと思った。 JavaScript 風に扱えるといえばよいのかな。