From 97b15a63a37ddc1ef185e93393b48f86710428f4 Mon Sep 17 00:00:00 2001 From: Eric Li Date: Sat, 24 Feb 2024 11:47:50 +0800 Subject: [PATCH] Add Android WebView post --- content/posts/android-webview.md | 265 +++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 content/posts/android-webview.md diff --git a/content/posts/android-webview.md b/content/posts/android-webview.md new file mode 100644 index 00000000..59cb828d --- /dev/null +++ b/content/posts/android-webview.md @@ -0,0 +1,265 @@ +--- +title: Android WebView 筆記 +date: 2024-02-24T11:45:00+08:00 +tags: +- Android +--- + +好幾年都沒有特別去用 Android 的 `WebView`,近期工作需要用到 `WebView`,所以特別去查一下並將資料放在這篇文章內方便日後翻查。 + +## AndroidX WebKit + +AndroidX 其實有 WebKit 的 artifact [`androidx.webkit:webkit`](https://maven.google.com/web/index.html#androidx.webkit:webkit),但不是把整個 browser 加到 app 入面(`WebView` 實際在用的 web browser 是由 [Google Play Store 提供](https://play.google.com/store/apps/details?id=com.google.android.webview),並可以 developer options 切換),而是把部分較新的 `WebView` 功能加個檢查 function,如果目前的 `WebView` 支援那個功能的話就可以執行這部分的 code。 + +AndroidX WebKit 入面有 `WebViewClientCompat`,這是用來 polyfill 部分 Android SDK 內的 `WebViewClient`。相信最值得一提的地方是 `shouldOverrideUrlLoading`。這個 method 是用來控制 `WebView` 是否載入這個網址。你可以在這個 callback 內截停某些網址載入,然後做其他東西。比如因應某些網址來 `startActivity` 讓那些網址改為系統瀏覽器開啟。另一個經典用法是用作由 JavaScript 傳遞資料給 native app,但如果經 URL 傳遞很長的 string 的話(例如 base64 encode 的圖片)很容易會 lag 機。 + +留意 `WebViewClientCompat` 的 `shouldOverrideUrlLoading` 跟 Android SDK 那個行為不同:`WebViewClientCompat` 會把除 `loadUrl` 的頁面都改由系統預設瀏覽器開啟。 + +## Lifecycle + +`WebView` 是一個比較麻煩的 UI 組件,因為要特別手動接駁 Android `Activity`/`Fragment` 的 lifecycle,如果做漏的話 `WebView` 的 state 就會在 configuration change 後出錯。就好似 Google Maps SDK 的 `MapView` 一樣[需要 forward lifecycle method](https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/MapView)。如果是 `Activity` 的話大概是這樣: + +```kotlin +class WebViewActivity : AppCompatActivity() { + private lateinit var webView: WebView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_webview) + webView = findViewById(R.id.webview) + + // 在載入 WebView 第一頁前要檢查 savedInstanceState + // 如果 configuration change 前用戶已經載入過或者按了連結去另一頁的話 + // 不檢查 savedInstanceState 就會在 configuration change 後載入第一頁 + if (savedInstanceState == null) { + webView.loadUrl("https://example.com") + + // 如果要加額外的 header(例如盜連 CodePen),可以在 loadUrl 再加 Map + webView.loadUrl( + "https://cdpn.io/RhinoLu/fullpage/RrPxMv?anon=true&view=", + mapOf("Referer" to "https://codepen.io/RhinoLu/pen/RrPxMv"), + ) + } + } + + override fun onPause() { + super.onPause() + webView.onPause() + } + + override fun onResume() { + super.onResume() + webView.onResume() + } + + override fun onDestroy() { + super.onDestroy() + webView.destroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + webView.saveState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + webView.restoreState(savedInstanceState) + } +} +``` + +## JavaScript 與 native app 雙向溝通 + +用 AndroidX WebKit 的主要原因是為了用 `WebMessageListener`。傳統的 JavaScript 與 native app 溝通方法是靠加了 [`@JavascriptInterface` annotation](https://developer.android.com/reference/android/webkit/JavascriptInterface) 的 class 做 JavaScript 向 native 傳遞訊息,由 native 向 JavaScript 傳遞訊息就用 [`WebView.evaluateJavascript`](https://developer.android.com/reference/android/webkit/WebView#evaluateJavascript(java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.String%3E)) call JavaScript 的 function。 + +自從 Android 6 (API Level 23) 開始,`WebView` 開始支援 `postWebMessage`,加上 AndroidX WebKit 有 `addWebMessageListener` method,我們可以用這兩個 method 實現 native app 和 JavaScript 雙向溝通。 + +### Native app 向 JavaScript 通訊 + +在 Android app 那邊,我們假設用戶按下 `sendData` 按鈕後就會發送一個帶有當前時間的 string: + +```kotlin +findViewById