html5与android之间相互调用
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+)@JavascriptInterfacefun showToast(message: String) {// 在主线程显示 Toast(WebView 回调在子线程,需切换)Handler(Looper.getMainLooper()).post {Toast.makeText(context, message, Toast.LENGTH_SHORT).show()}}@JavascriptInterfacefun openNativePage(pageName: String) {// 打开原生页面(如 Compose 页面或 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.com/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.net.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.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.example.webviewh5conn.ui.theme.Webviewh5connTheme
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Localeclass MainActivityold : ComponentActivity() {// 拍照相关变量private var currentPhotoPath: String? = nullprivate var h5PhotoCallback: String? = nullprivate 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) {// 获取相册选中的图片Urival selectedImageUri = result.data?.dataselectedImageUri?.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? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Webviewh5connTheme {Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {WebViewContainer()}}}}@Composablefun WebViewContainer() {val context = LocalContext.currentColumn(modifier = Modifier.fillMaxSize()) {Button(onClick = {val message = "Hello from Compose 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 = thisinitWebViewSettings(this, context)loadUrl("file:///android_asset/dist/index.html"); }},modifier = Modifier.fillMaxSize())}}@SuppressLint("SetJavaScriptEnabled")private fun initWebViewSettings(webView: WebView, context: Context) {val webSettings = webView.settingswebSettings.javaScriptEnabled = truewebSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOWwebSettings.allowFileAccess = truewebSettings.allowContentAccess = true// 允许访问ContentProvider资源(相册图片可能是content://格式)webSettings.allowFileAccessFromFileURLs = truewebSettings.allowUniversalAccessFromFileURLs = truewebView.addJavascriptInterface(AndroidInterface(this), "AndroidInterface")webView.webChromeClient = WebChromeClient()}inner class AndroidInterface(private val activity: MainActivityold) {@JavascriptInterfacefun showToast(message: String) {activity.runOnUiThread {Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()}}@JavascriptInterfacefun getAndroidInfo(): String {return "Android设备信息:型号=${Build.MODEL}"}@JavascriptInterfacefun takePhoto(callback: String) {activity.runOnUiThread {activity.h5PhotoCallback = callbackif (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 1001)} else {startCamera()}}}@JavascriptInterfacefun 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 (ContextCompat.checkSelfPermission(activity, requiredPermission)!= PackageManager.PERMISSION_GRANTED) {// 请求相册权限ActivityCompat.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)}}companion 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()}
}