如何在Compose跨平台中实现一个简易WebView

前端 0

背景

在当今的移动开发领域,Kotlin跨平台技术和Compose跨平台技术已成为备受关注的新兴技术。Kotlin作为一种现代编程语言,自从Google宣布支持以来,其在Android开发领域的地位逐渐上升。而Kotlin/Native的推出更是让Kotlin具备了跨平台的能力,这意味着开发人员可以在不同的平台上使用相同的代码库,从而提高开发效率。

基于Kotlin跨平台的能力,Compose跨平台技术应运而生。它借助Jetpack Compose框架设计,使开发人员在开发移动应用程序时可以同时适用于多个平台,如Android、iOS、Web和桌面等。通过使用统一的代码库和API,开发人员能够使用相同的代码构建应用程序,而无需为每个平台单独编写和维护不同的UI层。这种跨平台的特性使得开发人员可以更高效地开发和交付跨平台应用,降低开发成本并提高开发速度。

然而,在移动端开发中,WebView作为连接应用程序和Web内容的关键组件,却未能在Compose跨平台的组件库中找到其身影。为了填补这一空白,本文将详细介绍如何编写一个简易的WebView组件,以支持在Compose跨平台项目中展示Web页面。这将有助于开发人员充分利用WebView的功能,为用户提供丰富的交互体验。通过本文的讲解,相信开发人员能够更好地掌握如何在Compose跨平台技术中实现Web页面的嵌入和展示。

接口设计

为了实现一个跨平台的WebView,我们首先需要分析各个平台对WebView原生能力的支持程度。由于需要同时支持多个平台,我们只能抽离出双端都支持的能力,并在跨平台组件中提供相应支持。

具体而言,我们可以先定义一个通用的WebView接口,其中包含加载网页、执行JavaScript等常用方法。接下来,针对各个平台(如Android、iOS等),我们可以分别实现这个接口的具体版本。这样,通过调用通用的WebView接口,我们便能在不同平台上实现WebView功能。

首先,我们需要定义一个IWebView接口,以明确该跨平台WebView支持的能力。例如,loadUrl、evaluateJavaScript、startLoading等方法。通过这些方法,开发人员可以在不同平台上轻松地实现WebView功能,为用户提供更加丰富和流畅的Web体验。

interface IWebView {    /**     * True when the web view is able to navigate backwards, false otherwise.     */    fun canGoBack(): Boolean    /**     * True when the web view is able to navigate forwards, false otherwise.     */    fun canGoForward(): Boolean    /**     * Loads the given URL.     *     * @param url The URL of the resource to load.     */    fun loadUrl(        url: String,        additionalHttpHeaders: Map<String, String> = emptyMap(),    )    /**     * Navigates the webview back to the previous page.     */    fun goBack()    /**     * Navigates the webview forward after going back from a page.     */    fun goForward()    /**     * Reloads the current page in the webview.     */    fun reload()    /**     * Stops the current page load (if one is loading).     */    fun stopLoading()    /**     * Evaluates the given JavaScript in the context of the currently displayed page.     * and returns the result of the evaluation.     * Note: The callback will not be called from desktop platform because it is not supported by CEF currently.     */    fun evaluateJavaScript(        script: String,        callback: ((String) -> Unit)? = null,    )}

接下来我们需要在两端分别用原生能力提供实现:

  • Android端:

可以看到,我们使用了Webkit.WebView来提供Android端的原生Web能力

// androidMain/AndroidWebViewclass AndroidWebView(private val webView: WebView) : IWebView {    override fun canGoBack() = webView.canGoBack()    override fun canGoForward() = webView.canGoForward()    override fun loadUrl(        url: String,        additionalHttpHeaders: Map<String, String>,    ) {        webView.loadUrl(url, additionalHttpHeaders)    }    override fun goBack() {        webView.goBack()    }    override fun goForward() {        webView.goForward()    }    override fun reload() {        webView.reload()    }    override fun stopLoading() {        webView.stopLoading()    }    override fun evaluateJavaScript(        script: String,        callback: ((String) -> Unit)?,    ) {        val androidScript = "javascript:$script"        KLogger.i {            "evaluateJavaScript: $androidScript"        }        webView.post {            webView.evaluateJavascript(androidScript, callback)        }    }}
  • iOS

在iOS端,我们使用了WKWebView来提供原生Web能力

// iosMain/IOSWebViewclass IOSWebView(private val wkWebView: WKWebView) : IWebView {    override fun canGoBack() = wkWebView.canGoBack    override fun canGoForward() = wkWebView.canGoForward    override fun loadUrl(        url: String,        additionalHttpHeaders: Map<String, String>,    ) {        KLogger.d { "Load url: $url" }        val request =            NSMutableURLRequest.requestWithURL(                URL = NSURL(string = url),            )        additionalHttpHeaders.all { (key, value) ->            request.setValue(                value = value,                forHTTPHeaderField = key,            )            true        }        wkWebView.loadRequest(            request = request,        )    }    override fun goBack() {        wkWebView.goBack()    }    override fun goForward() {        wkWebView.goForward()    }    override fun reload() {        wkWebView.reload()    }    override fun stopLoading() {        wkWebView.stopLoading()    }    override fun evaluateJavaScript(        script: String,        callback: ((String) -> Unit)?,    ) {        wkWebView.evaluateJavaScript(script) { result, error ->            if (error != null) {                KLogger.e { "evaluateJavaScript error: $error" }                callback?.invoke(error.localizedDescription())            } else {                KLogger.i { "evaluateJavaScript result: $result" }                callback?.invoke(result?.toString() ?: "")            }        }    }}

这样,我们就完成了通用的WebView能力的接口设计和两端原生实现了。

WebViewState

接下来,我们需要设计一个状态(State)类,用于存储WebView的各种状态信息。这些状态信息包括需要加载的URL、头部信息(Header)等。通过这个状态类,我们可以方便地管理和更新WebView的状态,从而实现更加灵活和高效的跨平台WebView控制。

class WebViewState() {    var url: String? by mutableStateOf(null)    var headers: Map<String, String> by mutableStateOf(emptyMap())    var webview: IWebView? by mutableStateOf(null)}

Expect/Actual

在开始UI层的实现之前,我们需要先简单了解下Kotlin的Expect/Actual技术,它是UI层实现的基础。

Kotlin的Expect/Actual技术是为了跨平台开发而提供的强大特性。它允许开发者在共享代码库中定义期望(expect)的接口或抽象类,并在特定平台上提供实际(actual)的实现。这样,开发者在编写共享代码时,可以调用期望的接口或抽象类,无需关心具体实现。编译器会根据目标平台自动选择相应的实际实现。这极大地简化了跨平台开发的复杂性,同时保持了代码的一致性和可维护性。

下面是一个简单的示例,展示了如何使用expect/actual技术来实现一个跨平台的日志记录器:

// 共享模块expect class Logger() {    fun log(message: String)}// Android 平台实现actual class Logger actual constructor() {    actual fun log(message: String) {        android.util.Log.d("Logger", message)    }}// iOS 平台实现actual class Logger actual constructor() {    actual fun log(message: String) {        NSLog("Logger: %@", message)    }}// 使用日志记录器fun main() {    val logger = Logger()    logger.log("Hello, Kotlin!")}

在这个示例中,我们首先在共享模块中创建了一个Logger类,并声明了一个log函数。接下来,我们在Android和iOS平台上分别为这个函数提供了实际实现。在Android平台上,我们使用android.util.Log来记录日志;而在iOS平台上,我们则使用NSLog来实现同样的功能。

通过在共享模块中使用expect关键字声明接口,并在具体平台上使用actual关键字提供实现,我们实现了在不同平台上使用相同代码实现不同功能的灵活性。这使得开发跨平台应用变得更加简洁和高效。

UI层实现

接下来是UI层的实现。我们需要设计一个名为WebView的Composable组件,用于在Compose页面中展示Web页面。WebView组件接收WebViewState作为参数以获取所需数据,同时还具备modifier和生命周期回调功能,以便在适当的时候进行资源清理。这样一来,我们就可以在Compose中方便地实现Web页面的显示和管理。

@Composablefun WebView(    state: WebViewState,    modifier: Modifier = Modifier,    onCreated: () -> Unit = {},    onDispose: () -> Unit = {},){    ActualWebView(        state = state,        modifier = modifier,        onCreated = onCreated,        onDispose = onDispose,    )}

而它的实现也非常简单,直接将其代理给了另一个ActualWebView组件,它是一个expect组件,即其具体实现是交给各平台用原生能力实现的。

@Composableexpect fun ActualWebView(    state: WebViewState,    modifier: Modifier = Modifier,    onCreated: () -> Unit = {},    onDispose: () -> Unit = {},)

具体实现如下:

  • Android

在Android平台上,我们采用了AndroidX.WebView来实现Web页面的原生WebView渲染。接着,我们利用AndroidView实现了与Compose框架的桥接,从而在Compose页面中顺利地展示了Web内容。

@Composableactual fun ActualWebView(    state: WebViewState,    modifier: Modifier = Modifier,    onCreated: () -> Unit = {},    onDispose: () -> Unit = {},) {    AndroidView(        factory = { context ->            WebView(context).apply {                onCreated()            }.also {                state.webView = AndroidWebView(it)                state.webView.loadUrl(state.url, state.headers)            }        },        modifier = modifier,        onRelease = {            onDispose()        },    )}
  • iOS

在iOS平台上,我们采用了WKWebView来实现Web页面的原生WebView渲染。然后,我们利用UIKitView实现了与Compose框架的桥接,使得Web内容能够顺利地在Compose页面中展示。

@Composableactual fun ActualWebView(    state: WebViewState,    modifier: Modifier = Modifier,    onCreated: () -> Unit = {},    onDispose: () -> Unit = {},) {    UIKitView(        factory = {            WKWebView(                frame = CGRectZero.readValue(),                configuration = WKWebViewConfiguration(),            ).apply {                onCreated()            }.also {                state.webView = IOSWebView(it)                state.webView.loadUrl(state.url, state.headers)            }        },        modifier = modifier,        onRelease = {            onDispose()        },    )}

综上所述,我们成功地构建了一个简易的跨平台Compose WebView组件。这个组件能够支持基本的URL加载和JavaScript代码执行等功能。通过这个组件,开发者可以在Compose框架下轻松地实现跨平台Web页面的展示和管理,从而提高开发效率和用户体验。

使用

让我们来看下怎么使用这个组件:

MaterialTheme {    val webViewState = remember {        WebViewState().apply {             url = "https://github.com/KevinnZou/compose-webview-multiplatform"        }    }    WebView(        state = webViewState,        modifier = Modifier.fillMaxSize(),    )}

可以看到,只需要创建一个WebViewState并提供URL,然后使用WebView组件展示即可。

总结

本文介绍了一种在Compose跨平台项目中设计和实现跨端WebView的方法。逻辑层的核心在于抽象两端通用的Web能力,并利用Expect/Actual技术借助平台原生能力进行实现。UI层同样采用expect/actual技术,通过两端原生的WebView组件完成Web页面的渲染。

需要注意的是本文仅实现了一个基础的WebView,还有很多功能尚未涉及,例如Cookie支持、URL拦截、JS与原生通信等。此外,为了简化代码,示例中的WebView数据处理并非完全遵循响应式设计。然而,这些问题在我开源的WebView组件库中都已经得到解决,并且它还提供了对桌面端的支持。欢迎大家使用这个开源库,并提出宝贵建议,共同完善Compose跨平台WebView的功能!

https://github.com/KevinnZou/compose-webview-multiplatform

也许您对下面的内容还感兴趣: