Android原生App和WebView的交互方式詳解
一、前言
在移動(dòng)開(kāi)發(fā)中,我們有時(shí)候會(huì)遇到這樣的需求:
- 有一部分功能需要網(wǎng)頁(yè)實(shí)現(xiàn)(比如登錄頁(yè)、主頁(yè),已經(jīng)有網(wǎng)頁(yè)端了,不希望在 app 中再寫(xiě)一遍)
- 另一部分功能需要原生實(shí)現(xiàn)(比如硬件訪問(wèn)、獲取系統(tǒng)權(quán)限、或者一些注重性能的邏輯)
這時(shí)候 Hybrid App(原生 + WebView 混合應(yīng)用) 就派上用場(chǎng)了。
本文帶你全面了解 Android 原生 App 和 WebView 的交互方式,并附上實(shí)戰(zhàn)示例。
二、交互
WebView 與原生 App 的交互也就兩種:
- 網(wǎng)頁(yè)調(diào)用 App 原生方法(JS → Native)
- App 調(diào)用網(wǎng)頁(yè) JS 方法(Native → JS)
雙向通信的典型場(chǎng)景:
| 場(chǎng)景 | 方向 | 示例 |
|---|---|---|
| 網(wǎng)頁(yè)點(diǎn)擊按鈕調(diào)用 app 功能 | JS → Native | window.myApp.nativeMethod('a') |
| App 收集設(shè)備信息反饋給網(wǎng)頁(yè) | Native → JS | webView.evaluateJavascript("jsMethod('a', 'b')") |
| 登錄狀態(tài)同步 | 雙向 | 網(wǎng)頁(yè)通知 App 用戶登錄了,App 也可以主動(dòng)查詢網(wǎng)頁(yè)是否已登錄 |
2.1 編寫(xiě)本地 html
寫(xiě)一個(gè)本地的 html 文件 test_login.html,內(nèi)容如下:
<html>
<head><meta charset="utf-8"><title>Login Demo</title></head>
<body>
<h2>Hybrid Login Demo</h2>
<button onclick="login()">Login</button>
<button onclick="logout()">Logout</button>
<script>
window.loginState = { isLoggedIn: false };
window.isUserLoggedIn = function() {
console.log("isUserLoggedIn = " + window.loginState.isLoggedIn);
return window.loginState.isLoggedIn;
}
function login() {
window.loginState.isLoggedIn = true;
console.log("Login success!");
if (window.myApp && window.myApp.onLoginStateChanged) {
window.myApp.onLoginStateChanged(true);
}
}
function logout() {
window.loginState.isLoggedIn = false;
console.log("Logout success!");
if (window.myApp && window.myApp.onLoginStateChanged) {
window.myApp.onLoginStateChanged(false);
}
}
</script>
</body>
</html>
運(yùn)行效果:

可以看到,頁(yè)面內(nèi)容很簡(jiǎn)單,一個(gè) title,兩個(gè)按鈕。一個(gè)用于登入,一個(gè)用于登出。
html 中維護(hù)了一個(gè) loginState.isLoggedIn 屬性,表示用戶是否已登錄。
提供了一個(gè) isUserLoggedIn 函數(shù),用于查詢當(dāng)前登錄狀態(tài)。
另外,還有一個(gè) login 和一個(gè) logout 方法,分別用于模擬登入登出,當(dāng)狀態(tài)改變后,通過(guò) window.myApp.onLoginStateChanged 回調(diào)通知 app 登陸狀態(tài)發(fā)生了改變。
2.2 編寫(xiě) app
為了便于測(cè)試,我們將 test_login.html 文件,放在 assets 文件夾下,app 上的 WebView 直接加載本地 url 即可。
MainActivity 完整代碼:
package com.example.interaction
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.example.interaction.ui.theme.WebViewJsInteractionDemoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
WebViewJsInteractionDemoTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
LoginWebView(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun LoginWebView(modifier: Modifier = Modifier) {
var loginStatus by remember { mutableStateOf("Unknown") }
val context = LocalContext.current
val webViewRef = remember { mutableStateOf<WebView?>(null) }
Column(modifier = modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = true
allowContentAccess = true
cacheMode = WebSettings.LOAD_DEFAULT
}
webChromeClient = WebChromeClient()
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// Query login status when page is loaded
evaluateJavascript("isUserLoggedIn()") { result ->
val isLoggedIn = result?.contains("true") == true
loginStatus = if (isLoggedIn) "Logged In" else "Logged Out"
}
}
}
// Register the JavaScript interface
addJavascriptInterface(object {
@android.webkit.JavascriptInterface
fun onLoginStateChanged(isLoggedIn: Boolean) {
(context as ComponentActivity).runOnUiThread {
loginStatus = if (isLoggedIn) "Logged In" else "Logged Out"
Toast.makeText(context, "Login status changed: $loginStatus", Toast.LENGTH_SHORT).show()
}
}
}, "myApp")
WebView.setWebContentsDebuggingEnabled(true)
CookieManager.getInstance().setAcceptCookie(true)
loadUrl("file:///android_asset/test_login.html")
webViewRef.value = this
}
}
)
Spacer(modifier = Modifier.height(16.dp))
// Check login status button
Button(
onClick = {
webViewRef.value?.evaluateJavascript("isUserLoggedIn()") { result ->
val isLoggedIn = result?.contains("true") == true
loginStatus = if (isLoggedIn) "Logged In" else "Logged Out"
Toast.makeText(context, "Login status: $loginStatus", Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text("Check Login Status")
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Current Status: $loginStatus",
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
運(yùn)行效果:

可以看到,在 MainActivity 中,通過(guò) addJavascriptInterface 函數(shù)添加了 onLoginStateChanged 接口供 Web 端調(diào)用,添加接口時(shí),第二個(gè)參數(shù)是 name,Web 端將通過(guò) name.接口名 來(lái)調(diào)用對(duì)應(yīng)的接口,例如:window.myApp.onLoginStateChanged(true);。
在點(diǎn)擊 Check Login Status 按鈕后,通過(guò) WebView 的 evaluateJavascript 函數(shù)調(diào)用網(wǎng)頁(yè)端的 isUserLoggedIn 函數(shù),收到 result 后,更新 loginStatus 變量。
另外,還自定義了 WebViewClient,在 onPageFinished 調(diào)用后,主動(dòng)調(diào)用一次 isUserLoggedIn 函數(shù),完成 Current Status 的初始化。
三、后話
有一些需要注意的點(diǎn):
- 調(diào)用 js 方法時(shí),結(jié)果是異步返回的,通過(guò) listener 接收結(jié)果。
@JavascriptInterface的方法在 非 UI 線程 執(zhí)行,如果要更新 UI,需要使用runOnUiThread。- 設(shè)置了
WebView.setWebContentsDebuggingEnabled(true)之后,通過(guò) Chrome DevTools 可直接調(diào)試 WebView。方法是在 app 加載了網(wǎng)頁(yè)后,在 Chrome 瀏覽器訪問(wèn)chrome://inspect/#devices,在這里找到自己的設(shè)備,點(diǎn)擊 inspect。我對(duì)這種方式不是很熟悉,就不過(guò)多介紹了。

附:一些常見(jiàn)的問(wèn)題
注:不保真
在 WebView 中,通過(guò) webView.settings 可以獲取到 WebSettings,它可以用來(lái)配置一系列網(wǎng)頁(yè)渲染與訪問(wèn)能力。以下是關(guān)鍵屬性解釋:
| 屬性 | 作用 | 是否常用 | 注意事項(xiàng) |
|---|---|---|---|
javaScriptEnabled = true | 啟用網(wǎng)頁(yè)中的 JavaScript 執(zhí)行。沒(méi)有這個(gè),網(wǎng)頁(yè)的交互和動(dòng)態(tài)內(nèi)容幾乎全失效。 | ? 必須 | 啟用 JS 后要配合 addJavascriptInterface 謹(jǐn)慎使用,否則存在安全隱患。 |
domStorageEnabled = true | 啟用 HTML5 的 DOM Storage(localStorage / sessionStorage)。網(wǎng)頁(yè)才能保存本地狀態(tài)。 | ? 常用 | 現(xiàn)代 Web 必備。 |
databaseEnabled = true | 啟用 Web SQL 數(shù)據(jù)庫(kù)(舊標(biāo)準(zhǔn))。 | ?? 較舊 | 新網(wǎng)頁(yè)一般用 IndexedDB。 |
allowFileAccess = true | 允許訪問(wèn)本地文件(file://)。 | ? 常用 | 某些 WebView 資源加載或本地調(diào)試需要。 |
allowContentAccess = true | 允許訪問(wèn) content:// URI 內(nèi)容(如系統(tǒng)媒體)。 | ? 常用 | 安全風(fēng)險(xiǎn)低。 |
allowFileAccessFromFileURLs = true | 允許網(wǎng)頁(yè) JS 從 file:// 頁(yè)面訪問(wèn)其他本地文件。 | ?? 慎用 | 容易被惡意網(wǎng)頁(yè)利用本地文件。 |
allowUniversalAccessFromFileURLs = true | 允許 file:// 頁(yè)面訪問(wèn)任意網(wǎng)絡(luò)資源(http/https)。 | ?? 高風(fēng)險(xiǎn) | 建議僅限調(diào)試環(huán)境啟用。 |
useWideViewPort = true | 啟用自適應(yīng)寬度,讓網(wǎng)頁(yè)以「網(wǎng)頁(yè)比例」顯示而非手機(jī)分辨率。 | ? 常用 | 與 loadWithOverviewMode 一起使用更佳。 |
loadWithOverviewMode = true | 縮放網(wǎng)頁(yè)以適配屏幕寬度。 | ? 常用 | 常配合 responsive 頁(yè)面。 |
setSupportZoom(true) | 支持縮放。 | ? 常用 | 可搭配手勢(shì)操作。 |
builtInZoomControls = true | 啟用內(nèi)建縮放按鈕。 | ? 可選 | 通常在調(diào)試或舊網(wǎng)頁(yè)中啟用。 |
displayZoomControls = false | 隱藏默認(rèn)的縮放控件(僅保留手勢(shì)縮放)。 | ? 推薦 | 提升視覺(jué)體驗(yàn)。 |
cacheMode = WebSettings.LOAD_DEFAULT | 啟用緩存策略。 | ? 常用 | 可選 LOAD_NO_CACHE 禁止緩存。 |
WebViewClient 和 WebChromeClient 的區(qū)別:
| 對(duì)比項(xiàng) | WebViewClient | WebChromeClient |
|---|---|---|
| 職責(zé) | 控制頁(yè)面導(dǎo)航與加載邏輯 | 控制網(wǎng)頁(yè)中“瀏覽器行為”與 UI 事件 |
| 常用回調(diào) | shouldOverrideUrlLoading、onPageStarted、onPageFinished、onReceivedError | onProgressChanged、onReceivedTitle、onConsoleMessage、onJsAlert |
| 場(chǎng)景舉例 | 攔截跳轉(zhuǎn)、處理自定義 URL Scheme、控制加載動(dòng)畫(huà) | 顯示網(wǎng)頁(yè)標(biāo)題、監(jiān)控加載進(jìn)度、攔截 JS 彈窗、打印調(diào)試信息 |
| 比喻 | 瀏覽器“司機(jī)” | 瀏覽器“儀表盤(pán)” |
| 建議 | 必須設(shè)置一個(gè)(否則無(wú)法處理跳轉(zhuǎn)) | 可選(但調(diào)試與交互建議加) |
總結(jié)一句話:
WebViewClient 負(fù)責(zé)“頁(yè)面去哪”,WebChromeClient 負(fù)責(zé)“頁(yè)面看起來(lái)怎樣”。
其他關(guān)鍵配置:
| 配置 | 作用 |
|---|---|
setLayerType(View.LAYER_TYPE_HARDWARE, null) | 啟用硬件加速,提升渲染性能(尤其是視頻或動(dòng)畫(huà))。 |
setOnLongClickListener { true } + isLongClickable = false | 禁用長(zhǎng)按(防止復(fù)制或保存圖片)。 |
WebView.setWebContentsDebuggingEnabled(true) | 允許通過(guò) Chrome 調(diào)試網(wǎng)頁(yè)內(nèi)容(chrome://inspect)。 |
CookieManager.getInstance().setAcceptThirdPartyCookies(...) |
以上就是Android原生App和WebView的交互方式詳解的詳細(xì)內(nèi)容,更多關(guān)于Android App和WebView交互的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android簡(jiǎn)單實(shí)現(xiàn)啟動(dòng)畫(huà)面的方法
這篇文章主要介紹了Android簡(jiǎn)單實(shí)現(xiàn)啟動(dòng)畫(huà)面的方法,結(jié)合實(shí)例形式分析了啟動(dòng)畫(huà)面核心代碼及相關(guān)函數(shù),具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
Android獲取系統(tǒng)時(shí)間以及網(wǎng)絡(luò)時(shí)間
這篇文章主要為大家詳細(xì)介紹了Android獲取系統(tǒng)時(shí)間以及網(wǎng)絡(luò)時(shí)間的方法,感興趣的小伙伴們可以參考一下2016-07-07
Android實(shí)現(xiàn)打開(kāi)手機(jī)淘寶并自動(dòng)識(shí)別淘寶口令彈出商品信息功能
最近項(xiàng)目經(jīng)理給我們安排一個(gè)活兒,基于Android開(kāi)發(fā)實(shí)現(xiàn)打開(kāi)手機(jī)淘寶,并自動(dòng)識(shí)別淘口令,彈出商品信息,今天小編就抽空給大家分享下這個(gè)需求是怎么實(shí)現(xiàn)的,需要的朋友參考下吧2017-11-11
安卓(Android) 監(jiān)聽(tīng)按鍵長(zhǎng)按事件
本文主要介紹Android 中監(jiān)聽(tīng)按鍵的長(zhǎng)按事件,對(duì)按鍵機(jī)制進(jìn)行詳解,并附有代碼實(shí)例,具有參考價(jià)值,希望能幫到有需要的小伙伴2016-07-07
Android?Studio實(shí)現(xiàn)簡(jiǎn)單頁(yè)面跳轉(zhuǎn)的詳細(xì)教程
這篇文章主要給大家介紹了關(guān)于Android?Studio實(shí)現(xiàn)簡(jiǎn)單頁(yè)面跳轉(zhuǎn)的詳細(xì)教程,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Android?Studio具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-01-01
android實(shí)現(xiàn)定位與目的地的導(dǎo)航示例代碼
本篇文章主要介紹了android實(shí)現(xiàn)定位與目的地的導(dǎo)航示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02

