H5 与 Android 原生之间的相互调用是混合开发的核心场景,常用方式包括 H5 调用 Android 方法(通过 JavaScriptInterface)和 Android 调用 H5 方法(通过 evaluateJavascript)。
一、H5 调用 Android 原生方法(H5 → Android)
通过 Android 给 WebView 注册 JavaScriptInterface 接口,H5 可直接调用该接口中的方法,传递参数或触发原生逻辑。
1. 基础用法:传递简单参数(字符串、数字)
Android 侧:定义接口并注册到 WebView
kotlin
// 1. 定义交互接口类
class AndroidInterface(private val context: Context) {
// 注解 @JavascriptInterface 必须添加(API 17+)
@JavascriptInterface
fun showToast(message: String) {
// 在主线程显示 Toast(WebView 回调在子线程,需切换)
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun openNativePage(pageName: String) {
// 打开原生页面(如 ***pose 页面或 Activity)
Handler(Looper.getMainLooper()).post {
when (pageName) {
"setting" -> context.startActivity(Intent(context, SettingActivity::class.java))
"user" -> context.startActivity(Intent(context, UserActivity::class.java))
}
}
}
}
// 2. 在 WebView 中注册接口
WebView(context).apply {
settings.javaScriptEnabled = true // 必须开启 JS
// 注册接口:H5 中通过 window.AndroidInterface 调用
addJavascriptInterface(AndroidInterface(context), "AndroidInterface")
}
H5 侧:调用原生方法
javascript
运行
// 调用原生 Toast
window.AndroidInterface.showToast("这是 H5 触发的原生 Toast");
// 调用原生打开页面
document.getElementById("openSetting").onclick = function() {
window.AndroidInterface.openNativePage("setting");
};
2. 传递复杂参数(JSON 对象)
H5 传递 JSON 字符串,原生解析为对象;或原生返回 JSON 给 H5。
Android 侧:接收 JSON 并解析
kotlin
@JavascriptInterface
fun submitForm(formJson: String) {
// 用 Gson 解析 JSON 字符串
val gson = Gson()
val formData = gson.fromJson(formJson, FormData::class.java) // FormData 是数据类
// 处理表单数据(如提交到服务器)
Handler(Looper.getMainLooper()).post {
Log.d("H5交互", "收到表单:${formData.username}, ${formData.phone}")
}
}
// 数据类
data class FormData(val username: String, val phone: String, val address: String)
H5 侧:传递 JSON 字符串
javascript
运行
// 构造表单数据
const formData = {
username: "张三",
phone: "13800138000",
address: "北京市"
};
// 转成 JSON 字符串传递(避免参数格式错误)
window.AndroidInterface.submitForm(JSON.stringify(formData));
3. 原生回调 H5(带返回值)
H5 调用原生方法时,传递一个回调函数名,原生处理完成后调用该 H5 函数返回结果。
Android 侧:调用 H5 回调函数
kotlin
@JavascriptInterface
fun getNativeConfig(callbackName: String) {
// 原生获取配置信息
val config = mapOf(
"appVersion" to "1.0.0",
"isLogin" to true,
"theme" to "dark"
)
val configJson = Gson().toJson(config) // 转 JSON 字符串
// 调用 H5 的回调函数(通过 evaluateJavascript)
Handler(Looper.getMainLooper()).post {
webView.evaluateJavascript(
"window.$callbackName($configJson);", // 执行 H5 回调
null
)
}
}
H5 侧:定义回调并接收结果
javascript
运行
// 定义回调函数(挂载到 window 上)
window.onConfigReceived = function(config) {
console.log("原生配置:", config);
// 处理配置(如更新页面主题)
if (config.theme === "dark") {
document.body.classList.add("dark-mode");
}
};
// 调用原生方法,传递回调函数名
window.AndroidInterface.getNativeConfig("onConfigReceived");
二、Android 调用 H5 方法(Android → H5)
Android 通过 WebView.evaluateJavascript() 执行 H5 中的全局方法,传递参数或获取返回值。
1. 调用 H5 无参方法
H5 侧:定义全局方法
javascript
运行
// 全局方法:刷新页面数据
window.refreshPage = function() {
console.log("原生触发页面刷新");
// 执行刷新逻辑(如重新请求接口)
fetchData();
};
Android 侧:调用该方法
kotlin
// 在需要时(如原生数据更新后)调用
webView.evaluateJavascript(
"window.refreshPage();", // 执行 H5 方法
null // 无返回值时可忽略回调
)
2. 传递参数给 H5 方法
H5 侧:定义带参方法
javascript
运行
// 全局方法:更新用户信息显示
window.updateUserInfo = function(userInfo) {
document.getElementById("username").innerText = userInfo.name;
document.getElementById("avatar").src = userInfo.avatar;
};
Android 侧:传递参数(JSON 格式)
kotlin
// 构造用户信息
val userInfo = mapOf(
"name" to "李四",
"avatar" to "https://example.***/avatar.png"
)
val userJson = Gson().toJson(userInfo) // 转 JSON 字符串
// 调用 H5 方法并传递参数
webView.evaluateJavascript(
"window.updateUserInfo($userJson);", // 注意参数无需引号(JSON 本身带引号)
null
)
3. 获取 H5 方法的返回值
H5 侧:定义有返回值的方法
javascript
运行
// 全局方法:获取当前页面标题
window.getPageTitle = function() {
return document.title; // 返回页面标题
};
Android 侧:接收返回值(通过回调)
kotlin
webView.evaluateJavascript("window.getPageTitle();") { result ->
// result 是 H5 返回的字符串(带双引号,需处理)
val title = result.replace("\"", "") // 去除引号
Log.d("H5返回", "当前页面标题:$title")
}
三、通用注意事项
-
线程问题:
-
JavaScriptInterface的方法运行在 WebView 子线程,若需更新 UI(如显示 Toast、跳转页面),需切换到主线程(用Handler或runOnUiThread)。 -
evaluateJavascript的回调也运行在子线程,更新 UI 需同样处理。
-
-
安全问题:
-
addJavascriptInterface在 API < 17 时有安全漏洞(可能被恶意 H5 利用),需确保只加载可信 H5,或升级最小支持版本(API 17+)。 - 传递敏感信息(如 Token)时,避免明文传输,可加密后传递。
-
-
参数格式:
- 字符串参数需用双引号包裹(如
window.showToast("消息"))。 - 复杂对象必须序列化为 JSON 字符串,避免语法错误。
- 字符串参数需用双引号包裹(如
-
调试技巧:
- H5 侧:用
console.log输出日志,通过 Chrome 开发者工具(chrome://inspect)查看。 - Android 侧:用
WebChromeClient监听 H5 日志:kotlin
webChromeClient = object : WebChromeClient() { override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { Log.d("H5日志", "${consoleMessage.message()}") return super.onConsoleMessage(consoleMessage) } }
- H5 侧:用
四、典型应用场景
| 场景 | 调用方向 | 示例方法 |
|---|---|---|
| 显示原生弹窗(Toast/Dialog) | H5 → Android |
showToast(message)、showDialog(title, content)
|
| 打开原生页面 / 功能 | H5 → Android |
openCamera()、openMap(location)、openNativePage(pageName)
|
| 同步登录状态 | 双向 | H5 传 Token 给原生(setToken(token));原生传用户信息给 H5(updateUserInfo(user)) |
| 原生触发 H5 刷新 | Android → H5 |
refreshPage()、reloadData()
|
| 传递设备信息 | Android → H5 |
getDeviceInfo()(返回设备型号、系统版本等) |
通过上述方法,可实现 H5 与 Android 原生的灵活交互,满足混合开发的大部分需求。
三、截图展示及代码
效果图:
相关代码:
import android.Manifest
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.***.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import androidx.activity.***ponentActivity
import androidx.activity.***pose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.***pose.foundation.layout.Column
import androidx.***pose.foundation.layout.fillMaxSize
import androidx.***pose.foundation.layout.fillMaxWidth
import androidx.***pose.foundation.layout.padding
import androidx.***pose.material3.Button
import androidx.***pose.material3.MaterialTheme
import androidx.***pose.material3.Surface
import androidx.***pose.material3.Text
import androidx.***pose.runtime.***posable
import androidx.***pose.ui.Modifier
import androidx.***pose.ui.platform.LocalContext
import androidx.***pose.ui.unit.dp
import androidx.***pose.ui.viewinterop.AndroidView
import androidx.core.app.Activity***pat
import androidx.core.content.Context***pat
import androidx.core.content.FileProvider
import ***.example.webviewh5conn.ui.theme.Webviewh5connTheme
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivityold : ***ponentActivity() {
// 拍照相关变量
private var currentPhotoPath: String? = null
private var h5PhotoCallback: String? = null
private val takePictureLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
currentPhotoPath?.let { path ->
val imageUri = getImageUri(File(path))
Toast.makeText(this, "拍照成功: ${imageUri.toString()}", Toast.LENGTH_LONG).show()
callH5Callback(imageUri.toString())
}
} else {
callH5Callback("error")
}
}
private val pickPictureLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK && result.data != null) {
// 获取相册选中的图片Uri
val selectedImageUri = result.data?.data
selectedImageUri?.let { uri ->
Toast.makeText(this, "选中图片: ${uri.toString()}", Toast.LENGTH_LONG).show()
callH5Callback(uri.toString()) // 直接返回Uri给H5
} ?: run {
callH5Callback("error")
}
} else {
callH5Callback("error")
}
}
private fun callH5Callback(imagePath: String) {
h5PhotoCallback?.let { callback ->
webView?.evaluateJavascript("javascript:$callback('$imagePath')") {}
}
h5PhotoCallback = null
}
// 生成图片Uri(适配7.0+)
private fun getImageUri(file: File): Uri {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
} else {
Uri.fromFile(file)
}
}
private var webView: WebView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Webviewh5connTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WebViewContainer()
}
}
}
}
@***posable
fun WebViewContainer() {
val context = LocalContext.current
Column(
modifier = Modifier.fillMaxSize()
) {
Button(
onClick = {
val message = "Hello from ***pose Android!"
webView?.evaluateJavascript("javascript:showMessage('$message')") { result ->
Toast.makeText(context, "H5返回: $result", Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(top = 50.dp, start = 20.dp, end = 20.dp)
) {
Text("Android调用H5方法")
}
AndroidView(
factory = { ctx ->
WebView(ctx).apply {
webView = this
initWebViewSettings(this, context)
loadUrl("file:///android_asset/dist/index.html");
}
},
modifier = Modifier.fillMaxSize()
)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebViewSettings(webView: WebView, context: Context) {
val webSettings = webView.settings
webSettings.javaScriptEnabled = true
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
webSettings.allowFileA***ess = true
webSettings.allowContentA***ess = true
// 允许访问ContentProvider资源(相册图片可能是content://格式)
webSettings.allowFileA***essFromFileURLs = true
webSettings.allowUniversalA***essFromFileURLs = true
webView.addJavascriptInterface(AndroidInterface(this), "AndroidInterface")
webView.webChromeClient = WebChromeClient()
}
inner class AndroidInterface(private val activity: MainActivityold) {
@JavascriptInterface
fun showToast(message: String) {
activity.runOnUiThread {
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun getAndroidInfo(): String {
return "Android设备信息:型号=${Build.MODEL}"
}
@JavascriptInterface
fun takePhoto(callback: String) {
activity.runOnUiThread {
activity.h5PhotoCallback = callback
if (Context***pat.checkSelfPermission(activity, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED
) {
Activity***pat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 1001)
} else {
startCamera()
}
}
}
@JavascriptInterface
fun pickPhoto(callback: String) {
activity.runOnUiThread {
activity.h5PhotoCallback = callback
// 检查相册权限(Android 13+需要READ_MEDIA_IMAGES权限)
val requiredPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (Context***pat.checkSelfPermission(activity, requiredPermission)
!= PackageManager.PERMISSION_GRANTED
) {
// 请求相册权限
Activity***pat.requestPermissions(activity, arrayOf(requiredPermission), 1002)
} else {
// 已有权限,打开相册
openGallery()
}
}
}
}
// 启动相机(原有)
private fun startCamera() {
val photoFile = createImageFile() ?: run {
Toast.makeText(this, "无法创建图片文件", Toast.LENGTH_SHORT).show()
return
}
val photoUri = getImageUri(photoFile)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
takePictureLauncher.launch(intent)
}
// 打开相册
private fun openGallery() {
try {
val intent = Intent(Intent.ACTION_PICK).apply {
setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_MIME_TYPE)
}
pickPictureLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
// 处理没有找到合适应用的情况
Log.e("openGallery", "No activity found to handle image picking", e)
}
}
***panion object {
private const val IMAGE_MIME_TYPE = "image/*"
}
// 创建图片文件
private fun createImageFile(): File? {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date())
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return try {
File.createTempFile("JPEG_${timeStamp}_", ".jpg", storageDir).apply {
currentPhotoPath = absolutePath
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
@Deprecated("Deprecated in Java")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1001 -> { // 相机权限
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
callH5Callback("error")
Toast.makeText(this, "需要相机权限才能拍照", Toast.LENGTH_SHORT).show()
}
}
1002 -> { // 相册权限
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openGallery()
} else {
callH5Callback("error")
Toast.makeText(this, "需要相册权限才能选择照片", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
webView?.destroy()
}
}