WebView是我们在开发中经常会使用到的组件,我们可以用它来展示动态的Html页面。在Android的View体系中,我们可以直接在xml中添加WebView组件即可使用。但是在Jetpack Compose中,并没有可以直接使用的WebView组件。那么我们该如何在Compose中使用WebView呢?
幸运的是,现在已经有一个成熟的第三方库可以提供这些能力的支持。它提供了一个可以直接在Compose中使用的WebView组件。这样,开发者就不需要自行去实现WebView的封装逻辑。此外,它还支持Web页面属性的获取,加载状态的监听等能力,开箱即用,非常方便。
https://github.com/KevinnZou/compose-webview
基础使用
它的基础使用非常简单:
val state = rememberWebViewState("https://example.com")
WebView(
state
)
主要用到了两个关键的API,WebView用来提供UI布局,rememberWebViewState用来提供状态。
WebViewState
这是WebView组件的状态类,内部维护了关于WebView的状态属性。例如当前加载的Url,当前加载内容,加载状态,页面标题和icon以及错误状态等。
/**
* A state holder to hold the state for the WebView. In most cases this will be remembered
* using the rememberWebViewState(uri) function.
*/
@Stable
public class WebViewState(webContent: WebContent) {
public var lastLoadedUrl: String? by mutableStateOf(null)
internal set
/**
* The content being loaded by the WebView
*/
public var content: WebContent by mutableStateOf(webContent)
/**
* Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with
* progress) or the data loading has [LoadingState.Finished]. See [LoadingState]
*/
public var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)
internal set
/**
* Whether the webview is currently loading data in its main frame
*/
public val isLoading: Boolean
get() = loadingState !is Finished
/**
* The title received from the loaded content of the current page
*/
public var pageTitle: String? by mutableStateOf(null)
internal set
/**
* the favicon received from the loaded content of the current page
*/
public var pageIcon: Bitmap? by mutableStateOf(null)
internal set
/**
* A list for errors captured in the last load. Reset when a new page is loaded.
* Errors could be from any resource (iframe, image, etc.), not just for the main page.
* For more fine grained control use the OnError callback of the WebView.
*/
public val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()
/**
* The saved view state from when the view was destroyed last. To restore state,
* use the navigator and only call loadUrl if the bundle is null.
* See WebViewSaveStateSample.
*/
public var viewState: Bundle? = null
internal set
// We need access to this in the state saver. An internal DisposableEffect or AndroidView
// onDestroy is called after the state saver and so can't be used.
internal var webView by mutableStateOf<WebView?>(null)
}
而rememberWebViewState则是提供了一个基础的加载Url的WebViewState
/**
* Creates a WebView state that is remembered across Compositions.
*
* @param url The url to load in the WebView
* @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl].
* Note that these headers are used for all subsequent requests of the WebView.
*/
@Composable
public fun rememberWebViewState(
url: String,
additionalHttpHeaders: Map<String, String> = emptyMap()
): WebViewState =
// Rather than using .apply {} here we will recreate the state, this prevents
// a recomposition loop when the webview updates the url itself.
remember {
WebViewState(
WebContent.Url(
url = url,
additionalHttpHeaders = additionalHttpHeaders
)
)
}.apply {
this.content = WebContent.Url(
url = url,
additionalHttpHeaders = additionalHttpHeaders
)
}
有了WebViewState,我们就可以在外部去获取Web相关的属性并展示了:
Column {
val state = rememberWebViewState("https://example.com")
Text(text = "${state.pageTitle}")
val loadingState = state.loadingState
if (loadingState is LoadingState.Loading) {
LinearProgressIndicator(
progress = loadingState.progress,
modifier = Modifier.fillMaxWidth()
)
}
WebView(
state
)
}
WebViewNavigator
还有一个重要的类就是WebViewNavigator,它封装了WebView导航相关的能力并暴露给开发者。例如前进,后退,重新加载,停止加载等。此外,加载链接,Post数据,加载Html等基础的加载能力也封装在了这里,最终会传递到WebView完成链接的加载。
/**
* Allows control over the navigation of a WebView from outside the composable. E.g. for performing
* a back navigation in response to the user clicking the "up" button in a TopAppBar.
*
* @see [rememberWebViewNavigator]
*/
@Stable
public class WebViewNavigator(private val coroutineScope: CoroutineScope) {
/**
* True when the web view is able to navigate backwards, false otherwise.
*/
public var canGoBack: Boolean by mutableStateOf(false)
internal set
/**
* True when the web view is able to navigate forwards, false otherwise.
*/
public var canGoForward: Boolean by mutableStateOf(false)
internal set
public fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {
coroutineScope.launch {
navigationEvents.emit(
NavigationEvent.LoadUrl(
url,
additionalHttpHeaders
)
)
}
}
public fun loadHtml(
html: String,
baseUrl: String? = null,
mimeType: String? = null,
encoding: String? = "utf-8",
historyUrl: String? = null
) {
coroutineScope.launch {
navigationEvents.emit(
NavigationEvent.LoadHtml(
html,
baseUrl,
mimeType,
encoding,
historyUrl
)
)
}
}
public fun postUrl(
url: String,
postData: ByteArray
) {
coroutineScope.launch {
navigationEvents.emit(
NavigationEvent.PostUrl(
url,
postData
)
)
}
}
/**
* Navigates the webview back to the previous page.
*/
public fun navigateBack() {
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }
}
/**
* Navigates the webview forward after going back from a page.
*/
public fun navigateForward() {
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }
}
/**
* Reloads the current page in the webview.
*/
public fun reload() {
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }
}
/**
* Stops the current page load (if one is loading).
*/
public fun stopLoading() {
coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }
}
}
rememberWebViewNavigator则是提供了一个默认的navigator并保存在remember中
/**
* Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided
* override.
*/
@Composable
public fun rememberWebViewNavigator(
coroutineScope: CoroutineScope = rememberCoroutineScope()
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) }
使用上我们就可以通过navigator来操控WebView的前进后退了
Column {
val state = rememberWebViewState("https://example.com")
val navigator = rememberWebViewNavigator()
TopAppBar(
title = { Text(text = "WebView Sample") },
navigationIcon = {
if (navigator.canGoBack) {
IconButton(onClick = { navigator.navigateBack() }) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back"
)
}
}
}
)
Text(text = "${state.pageTitle}")
val loadingState = state.loadingState
if (loadingState is LoadingState.Loading) {
LinearProgressIndicator(
progress = loadingState.progress,
modifier = Modifier.fillMaxWidth()
)
}
WebView(
state = state,
navigator = navigator
)
}
WebView
最后让我们来看一下它的完整API:
/**
* A wrapper around the Android View WebView to provide a basic WebView composable.
*
* If you require more customisation you are most likely better rolling your own and using this
* wrapper as an example.
*
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it
* is incorrectly sizing, use the layoutParams composable function instead.
*
* @param state The webview state holder where the Uri to load is defined.
* @param modifier A compose modifier
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
* the WebView back.
* @param navigator An optional navigator object that can be used to control the WebView's
* navigation from outside the composable.
* @param onCreated Called when the WebView is first created, this can be used to set additional
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be
* subsequently overwritten after this lambda is called.
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved
* if you need to save and restore state in this WebView.
* @param client Provides access to WebViewClient via subclassing
* @param chromeClient Provides access to WebChromeClient via subclassing
* @param factory An optional WebView factory for using a custom subclass of WebView
*/
@Composable
public fun WebView(
state: WebViewState,
modifier: Modifier = Modifier,
captureBackPresses: Boolean = true,
navigator: WebViewNavigator = rememberWebViewNavigator(),
onCreated: (WebView) -> Unit = {},
onDispose: (WebView) -> Unit = {},
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
factory: ((Context) -> WebView)? = null,
)
可以看到,几个重要的参数已经在之前介绍过了,剩下的也都很好理解。最后一个参数要注意一下,它允许开发者提供一个工厂方法来创建自定义的WebView。如果工程中已经有定制好的WebView,可以通过这个方法传入。
WebView(
...
factory = { context -> CustomWebView(context) }
)
完整示例
class BasicWebViewSample : ComponentActivity() {
val initialUrl = "https://google.com"
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSampleTheme {
val state = rememberWebViewState(url = initialUrl)
val navigator = rememberWebViewNavigator()
var textFieldValue by remember(state.lastLoadedUrl) {
mutableStateOf(state.lastLoadedUrl)
}
Column {
TopAppBar(
title = { Text(text = "WebView Sample") },
navigationIcon = {
if (navigator.canGoBack) {
IconButton(onClick = { navigator.navigateBack() }) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back"
)
}
}
}
)
Row {
Box(modifier = Modifier.weight(1f)) {
if (state.errorsForCurrentRequest.isNotEmpty()) {
Image(
imageVector = Icons.Default.Error,
contentDescription = "Error",
colorFilter = ColorFilter.tint(Color.Red),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(8.dp)
)
}
OutlinedTextField(
value = textFieldValue ?: "",
onValueChange = { textFieldValue = it },
modifier = Modifier.fillMaxWidth()
)
}
Button(
onClick = {
textFieldValue?.let {
navigator.loadUrl(it)
}
},
modifier = Modifier.align(Alignment.CenterVertically)
) {
Text("Go")
}
}
val loadingState = state.loadingState
if (loadingState is LoadingState.Loading) {
LinearProgressIndicator(
progress = loadingState.progress,
modifier = Modifier.fillMaxWidth()
)
}
// A custom WebViewClient and WebChromeClient can be provided via subclassing
val webClient = remember {
object : AccompanistWebViewClient() {
override fun onPageStarted(
view: WebView,
url: String?,
favicon: Bitmap?
) {
super.onPageStarted(view, url, favicon)
Log.d("Accompanist WebView", "Page started loading for $url")
}
}
}
WebView(
state = state,
modifier = Modifier
.weight(1f),
navigator = navigator,
onCreated = { webView ->
webView.settings.javaScriptEnabled = true
},
client = webClient
)
}
}
}
}
}
下载
repositories {
mavenCentral()
}
dependencies {
implementation "io.github.KevinnZou:compose-webview:0.33.4"
}