Web Everywhere: Introducing our Compose Multiplatform WebView Library

Kevin Zou
7 min readSep 25, 2023

--

Background

WebView is a critical component within mobile development, facilitating the loading and rendering of web pages, HTML content display, and interaction with web-based resources.

Compose Multiplatform is a groundbreaking framework empowering developers to construct user interfaces across diverse platforms, such as Android, iOS, and desktop, utilizing Jetpack Compose — an Android UI toolkit. It fosters a unified codebase for UI development, obviating the need for platform-specific implementations. With Compose Multiplatform, developers can effortlessly fabricate reusable UI components and logic, seamlessly adaptable across multiple platforms.

Yet, within the JetBrains Compose multiplatform framework, there currently exists no direct provision for a WebView component. To present web pages, one must employ the expect/actual techniques and personally implement them on each platform, entailing an extensive workload.

To streamline the process of displaying web pages within Compose Multiplatform, I have developed this library that offers a WebView widget, directly employable within the shared module. Consequently, developers need only compose code once, and it will effortlessly function across all platforms.

Inspiration

This library draws inspiration from Accompanist’s web module, which is a library specifically developed to enhance the practical functionalities of Jetpack Compose. In its web module, Accompanist provides a WebView wrapper for Jetpack Compose, enabling web page display capabilities.

However, since Accompanist is built on top of the Jetpack Compose, it is limited to Android platforms. To ensure cross-platform compatibility, I have abstracted the platform-independent code into the shared module. In cases where functionality depends on platform-specific abilities, I first design a shared interface and then utilize the expect/actual technique to implement it for each platform.

On the Android side, the library delegates the core data loading capabilities to the native AndroidX WebView and encapsulates it within the AndroidView to achieve the desired user interface display. The iOS side follows a similar approach, utilizing the native WKWebView and UIKitView. For desktop platforms, the library utilizes JavaFx’s WebView and wraps it with JFXPanel.

The WebView provided by this library not only supports basic link loading but also enables HTML loading and JavaScript evaluations. Additionally, developers can access information such as page loading progress and title through WebViewState. Moreover, the library offers WebViewNavigator, allowing developers to control WebView navigation, including actions like forward, backward, and reload.

Basic Usage

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

The basic usage is as follows:

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

WebView(state)

This will display a WebView in your Compose layout that shows the URL provided.

WebView State

This library provides a WebViewState class as a state holder to hold the state for the WebView.

class WebViewState(webContent: WebContent) {
var lastLoadedUrl: String? by mutableStateOf(null)
internal set

/**
* The content being loaded by the WebView
*/
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]
*/
var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)
internal set

/**
* Whether the webview is currently loading data in its main frame
*/
val isLoading: Boolean
get() = loadingState !is LoadingState.Finished

/**
* The title received from the loaded content of the current page
*/
var pageTitle: String? 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.
*/
val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()

// 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<IWebView?>(null)
}

It can be created using the rememberWebViewState function, which can be remembered across Compositions.

val state = rememberWebViewState("https://github.com/KevinnZou/compose-webview-multiplatform")

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

Developers can use the WebViewState to get the loading information of the WebView, such as the loading progress, the loading status, and the URL of the current page.

Column {
val state = rememberWebViewState("https://github.com/KevinnZou/compose-webview-multiplatform")

Text(text = "${state.pageTitle}")
val loadingState = state.loadingState
if (loadingState is LoadingState.Loading) {
LinearProgressIndicator(
progress = loadingState.progress,
modifier = Modifier.fillMaxWidth()
)
}
WebView(
state
)
}

WebView Navigator

This library provides a WebViewNavigator class to 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. It can be used to load a new URL, reload the current URL, and go back and forward in the history.

class WebViewNavigator(private val coroutineScope: CoroutineScope) {

/**
* True when the web view is able to navigate backwards, false otherwise.
*/
var canGoBack: Boolean by mutableStateOf(false)
internal set

/**
* True when the web view is able to navigate forwards, false otherwise.
*/
var canGoForward: Boolean by mutableStateOf(false)
internal set

fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {}

fun loadHtml(
html: String,
baseUrl: String? = null,
mimeType: String? = null,
encoding: String? = "utf-8",
historyUrl: String? = null
) {
}

fun postUrl(
url: String,
postData: ByteArray
) {
}

/**
* Navigates the webview back to the previous page.
*/
fun navigateBack() {}

/**
* Navigates the webview forward after going back from a page.
*/
fun navigateForward() {}

/**
* Reloads the current page in the webview.
*/
fun reload() {}

/**
* Stops the current page load (if one is loading).
*/
fun stopLoading() {}
}

It can be created using the rememberWebViewNavigator function, which can be remembered across Compositions.

val navigator = rememberWebViewNavigator()

@Composable
public fun rememberWebViewNavigator(
coroutineScope: CoroutineScope = rememberCoroutineScope()
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) }

Developers can use the WebViewNavigator to control the navigation of the WebView.

val navigator = rememberWebViewNavigator()

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

API

The complete API of this library is as follows:

/**
*
* A wrapper around the Android View WebView to provide a basic WebView composable.
*
* @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.
* @param onDispose Called when the WebView is destroyed.
* @sample sample.BasicWebViewSample
*/
@Composable
fun WebView(
state: WebViewState,
modifier: Modifier = Modifier,
captureBackPresses: Boolean = true,
navigator: WebViewNavigator = rememberWebViewNavigator(),
onCreated: () -> Unit = {},
onDispose: () -> Unit = {},
)

Example

A simple example would be like this:

@Composable
internal fun WebViewSample() {
MaterialTheme {
val webViewState = rememberWebViewState("https://github.com/KevinnZou/compose-webview-multiplatform")
Column(Modifier.fillMaxSize()) {
val text = webViewState.let {
"${it.pageTitle ?: ""} ${it.loadingState} ${it.lastLoadedUrl ?: ""}"
}
Text(text)
WebView(
state = webViewState,
modifier = Modifier.fillMaxSize()
)
}

}
}

For a full example, please refer to BasicWebViewSample

Download

You can add this library to your project using Gradle.

Multiplatform

To add to a multiplatform project, add the dependency to the common source set:

repositories {
mavenCentral()
}

kotlin {
sourceSets {
commonMain {
dependencies {
// use api since the desktop app need to access the Cef to initialize it.
api("io.github.kevinnzou:compose-webview-multiplatform:1.3.0")
}
}
}
}

Single Platform

For an Android-only project, you directly can use my other library. Add the dependency to the app level build.gradle.kts:

repositories {
maven("https://jitpack.io")
}

dependencies {
implementation ("com.github.KevinnZou:compose-webview:0.33.2")
}

Summary

This project effectively encompasses all the functionalities offered by the Accompanist WebView library. Additionally, the library ensures a uniform API structure, thereby facilitating seamless migration of WebViews from Jetpack Compose projects to cross-platform projects. Currently, the project supports Android, iOS, and Desktop platforms.

--

--

Kevin Zou

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