読者です 読者をやめる 読者になる 読者になる

なにも わからぬ

パソコンとプログラミング関係をメモっていきたい

【KotlinでAndroid】 ガチ初心者がネットワークアクセスで四苦八苦

始めたばかりのガチ初心者が書いた記事なのでコードの品質は保証できません。参考にする場合は自分で調べ直してください。突っ込みを頂けたら追記・訂正します

ようやくAndroid Studioに手を出してみた。JavaもKotlinもAndroid開発もなにもかも未経験だったので、メインスレッドでネットワーク通信すると例外とか別スレッドでUIいじろうとすると例外とか、おそらく先人がみな経験したような地雷を丁寧に一つずつ踏み抜いていくことになった。ので調べたことをメモしていくことにする。

はじめの一歩

class MainActivity : AppCompatActivity() {

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

5行目のsetContentView()activity_main.xmlの内容が表示される、と同時にここで初めてviewの内容にコード側からアクセスできるようになる。javascriptで言えばここでhtmlを展開するからそれより前でDOMノード取得しようとしてもできないようなもんね。ここでsetContentViewより前でfindViewById()しようとして
f:id:htkb:20161106221157j:plain
となって悩んだ。

ネットワーク通信

setContentView()の後ろにtextview.setText("hogehoge")などとして(kotlin-android-extensions使用)viewの内容が書き換えられるのを確認後、OkHttpライブラリを使いhtmlをgetし表示しようと試みる。まずネットワークの権限がないとエラーが出るので事前にAndroidManifest.xml<manifest...の下の行に<uses-permission android:name="android.permission.INTERNET" />を追加しておく。んで

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
        textview.setText(getHtml())
    }
}

fun getHtml(): String {
    val client = OkHttpClient()
    val req = Request.Builder().url("http://www.google.co.jp").get().build()
    val resp = client.newCall(req).execute()
    return if(resp != null && resp.body() != null) resp.body().string() else "error"
}

とすると
f:id:htkb:20161106221157j:plain
ときたもんだ。Android Monitorを見るとandroid.os.NetworkOnMainThreadExceptionとのこと、これは超頻出問題らしく、ネットワーク通信はメインスレッドで行ってはならず非同期通信しなければならないらしい。で、この非同期通信のための最小コードがよくわからずぐぬぬぬしていたのだが、とりあえず問題なく動いたっぽいのが以下:

open class MyAsyncTask : AsyncTask<Void, Void, String>() {

    override fun doInBackground(vararg params: Void): String? {
        return null
    }
}

を頭に追加し、

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        object : MyAsyncTask() {
            override fun doInBackground(vararg params: Void): String? {
                return getHtml()
            }
        }.execute()
    }
}

とする。AsyncTaskを継承したMyAsynkTaskを即時実効しているようで、doInBackground()メソッドが実際に裏で動くタスクのよう?
参照:Kotlinを使ってみよう!(2) : OkHttp編 - Takahiro Octopress Blog

非同期スレッドでUIはいじれない

以上のコードはdoInBackground()がhtmlを返しているだけなのでまだ画面に反映されない。そこでreturnするまえにtextview.setText(getHtml())などとやってみると、
f:id:htkb:20161106221157j:plain
でログにはCaused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.と出る。でググってみるとやはり頻出のようで、整合性などの問題のためメインスレッド以外からUIの要素を変更したりできないようだ。そのかわり、AsyncTaskonPostExecute()メソッドはdoInBackground()が終わった後に返り値を受け取って自動的に呼ばれるようで、onPostExecute()ではUIの変更が可能なよう。そこで最終的に

open class MyAsyncTask : AsyncTask<Void, Void, String>() {

    override fun doInBackground(vararg params: Void): String? {
        return null
    }

    override fun onPostExecute(text: String) {}
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        object : MyAsyncTask() {
            override fun doInBackground(vararg params: Void): String? {
                return getHtml()
            }
            override fun onPostExecute(text: String) {
                textview.setText(text)
            }
        }.execute()
    }
}

fun getHtml(): String {
    val client = OkHttpClient()
    val req = Request.Builder().url("http://www.google.co.jp").get().build()
    val resp = client.newCall(req).execute()
    return if(resp != null && resp.body() != null) resp.body().string() else "error"
}

こうなってようやく、
f:id:htkb:20161106224347j:plain
生のhtmlをtextviewに垂れ流すまでに至った。ここまできてようやくkotlinでロジックを書き始められそう。難しいぜAndroid