Android で WebView メモ
最近 Android で WebView を触っているので…。
セットアップ
Android プロジェクトを作成する | Android デベロッパー | Android Developers を参考にして、プロジェクトを作成する。Empty Activity を選ぶ。
AndroidManifest.xml
app/manifests/AndroidManifest.xml
に必要なケーパビリティを設定する。
-
Manifest
の直下に、<uses-permission android:name="android.permission.INTERNET" />
を入れる必要あり。 - もし 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.kt
の AppCompatActivity
の onCreate
メソッドに 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.showToast
で showToast
メソッドを呼び出すことができる。
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