WebView for Jetpack Compose

Kevin Zou
7 min readJan 31, 2024

--

WebView is a component we often use in App development, which can be used to display dynamic HTML pages. In the Android View system, we can directly add the WebView component to XML to use it. But in Jetpack Compose, there is no WebView component that can be used directly. So how do we use WebView in Compose?

Fortunately, there was already a library that provided support for it. It includes a WebView component that can be directly used within Compose, eliminating the need for developers to create their own encapsulation logic for WebView. Furthermore, it offers functionalities such as fetching web page attributes and listening to loading states, all readily available out of the box.

Basic usage

To implement this wrapper there are two key APIs which are needed: WebView, which provides the layout, and rememberWebViewState(url) which provides some remembered state including the URL to display.

Its basic usage is very simple:

val state = rememberWebViewState("https://example.com")

WebView(
state
)

WebViewState

This is the state class of the WebView component, which maintains state properties about the WebView internally. For example, the currently loaded URL and content, the loading status, the page title and icon, and the error status.

/**
* 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 provides a basic WebViewState for loading URLs.

/**
* 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
)
}

With WebViewState, we can get Web-related properties externally and display them:

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

Another important class is WebViewNavigator, which encapsulates WebView navigation-related capabilities and exposes them to developers. For example, navigating forward and backward, reload, stop loading, etc. In addition, the basic loading capabilities such as loading URLs, Post data, and loading HTML are also encapsulated here. It will rely on AndroidX WebView to realize these functionalities internally.

/**
* 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 provides a default navigator and saves it in 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) }

Using it, we can control the forward and backward of WebView through the navigator:

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

Finally, let’s take a look at its complete 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,
)

As you can see, several important parameters have been introduced before, and the rest are easy to understand. The last parameter should be noted that it allows developers to provide a factory method to create custom WebViews. If there is already a customized WebView in the project, you can pass it in through this method.

WebView(
...
factory = { context -> CustomWebView(context) }
)

Complete example

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
)
}
}
}
}
}

Download

repositories {
mavenCentral()
}

dependencies {
implementation "io.github.KevinnZou:compose-webview:0.33.4"
}

Following Readings

--

--

Kevin Zou

Android Developer in NetEase. Focusing on Kotlin Multiplatform and Compose Multiplatform