😺

Android で WebView メモ

2021/01/17に公開

最近 Android で WebView を触っているので…。

セットアップ

Android プロジェクトを作成する  |  Android デベロッパー  |  Android Developers を参考にして、プロジェクトを作成する。Empty Activity を選ぶ。

AndroidManifest.xml

app/manifests/AndroidManifest.xml に必要なケーパビリティを設定する。

  1. Manifest の直下に、<uses-permission android:name="android.permission.INTERNET" /> を入れる必要あり。
  2. もし HTTP を使う場合は、application の属性に android:usesCleartextTraffic="true" を追加する

2 はそもそも HTTP 使うな危険、なのだが、開発の初期段階とか途中だと面倒なので使うよねという。ここでは指定してないが、細かく特定ドメインだけクリアテキストを許可することもできるようだけど、1 ファイル余計に作成しなければいけなくて、開発初期の実験だったので、ここでは一括でクリアテキストを有効にしている。

レイアウトに WebView を追加

app/res/layout/activity_main.xml で、既にある Text を消して、Widgets の WebView を設定する。コンポーネントカタログはソートされてないので、結構目では探しづらい。

MainActivity.kt

app/java/com.xxxxxxx/MainActivity.ktAppCompatActivityonCreate メソッドに WebView 関連を追加する

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // ここまでは元からあるコード

        // WebView コンポーネントを `findViewById` で探し出す
        val myWebView: WebView = findViewById(R.id.webview)

        // WebView で JavaScript を許可する
        myWebView.settings.javaScriptEnabled = true

        // WebView 内部でページ遷移をしようとしたときに呼び出されるやつ
        myWebView.setWebViewClient(object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                Log.i("HOGE", url)
                // ホワイトリストとか実装しておくと本運用ではよさそう
                return true
            }
        })

        // WebView として localhost:3000 を開く
        myWebView.loadUrl("http://localhost:3000")
    }
}

adb reverse proxy で tcp3000 をエミュレータからアクセスできるようにする

Android は自前でローカルホストを持つため、エミュレータであってもローカルホストは Android エミュレータ自分自身のアドレスになってしまう。対策としては、adb の reverse proxy を使う。一応 10.0.2.2 でアクセスできるらしいのだけど。

ここまでやると、localhost:3000 で動いてる SPA をそのまま動かすことができる。

WebView で URL 遷移を禁止・許可する

myWebView.setWebViewClient(object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
        Log.i("HOGE", url)
        return true
    }
})
  • Android API level23 だと shouldOverrideUrlLoading(view: WebView, url: String): Boolean をオーバーライドする。
  • Android API level24 以後 だと shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean をオーバーライドする(らしい)

return true で WebView のリンクの処理を中断させられる。return false でそのまま処理させることもできる。

WebView に機能を expose する

まず機能を expose するためのクラスを作成する

package com.example.myapplication

import android.content.Context
import android.webkit.JavascriptInterface
import android.widget.Toast

/** Instantiate the interface and set the context  */
class WebAppInterface(private val mContext: Context) {

    /** Show a toast from the web page  */
    @JavascriptInterface
    fun showToast(toast: String) {
        Toast.makeText(mContext, toast, Toast.LENGTH_LONG).show()
        return "hogeeeeee"
        // 戻り値は、WebView 側に渡される
    }
}

@JavascriptInterface アノテーションは、Android アプリの WebView 連携 - アカベコマイリ にも書いてあるが、全メソッドが expose されると脆弱になるため、expose するメソッドだけつける。

このファイル(クラス)を作成しておき MainActivity.kt の WebView 制御コードに myWebView.addJavascriptInterface(WebAppInterface(this), "Android") を追加すると、WebView 内の JS が window.Android.showToastshowToast メソッドを呼び出すことができる。

if ('Android' in window) {
  const res = (window as any).Android.showToast('fuga') as any
  console.log(JSON.stringify(res))
}

ただ window オブジェクトの型にもともとないものなため、真面目に実装しようと思うと Window global object の型を拡張する必要がある。どうせなら Kotlin のコードから自動生成できればいいのだけど。

複雑な型をやりとりする方法

大体世の中にある資料の大半では showToast の事例しかなく、引数が文字列型一つに収まっている。

文字列、数字、それらの配列はそのまま JavaScript 側と Kotlin 側で透過的に扱えるようだが、それより複雑なクラス・オブジェクトだとダメなので、現実的には、JSON でシリアライズ・デシリアライズする必要がある。

 @JavascriptInterface
fun hoge(json: String) {
    val hoge = Gson().fromJson(json, Hoge::class.java)
    xxxxHoge(hoge)
}

たとえばこのような感じに。

インターフェース決めたら、Kotlin 用のコードが自動生成されたり、TypeScript の型やコードが自動生成されればよいのにとは思う。やりとりする数が増えたら、そういうのを検討したほうがいいかもしれない。

Kotlin 側から働きかける方法

JavaScript 側で機能を expose (というほどでもないが、単に window オブジェクトに定義)してそれを loadUrl で呼び出す。

myWebView.loadUrl("javascript:window.hoge('fuga')")

動的に値を渡すときは JSON serialize しておくとよさそう。

if ('Android' in window) {
  (window as any).hoge = (message: string) => {
    // おそらく observer パターンあたりを使う
  }
}

Discussion