How to Seamlessly Embed Web Content in Android Apps — Part 3
In the first two parts of our series, we laid the groundwork for integrating web content into Android applications, exploring the fundamental concepts of both WebView and Custom Tabs, and then delving into the specifics and benefits of Custom Tabs for in-app Browse. Now, in this third installment, we turn our attention to WebView, focusing on its powerful capabilities for embedding web content directly within your application’s UI, with a particular emphasis on its usage within Jetpack Compose.
As a quick reminder, WebView is an Android system component that allows you to display web pages as part of your activity layout, offering a high degree of flexibility in customizing and updating the UI for web content you control. It acts like a mini-browser within your app, enabling seamless integration of existing web content.
While WebView offers unparalleled control over the displayed web content, integrating it seamlessly into a declarative UI framework like Compose presents its own set of considerations. This blog post will guide you through understanding WebView’s role in modern Android development, how it functions within a composable environment, and best practices for creating rich, web-powered experiences in your Compose applications.
WebView is particularly well-suited for scenarios where you need a high degree of control and flexibility over the web content displayed within your app.
Consider using WebView when:
- Embedding your own web content as primary or supporting content: If you own or control the web content and want to display it inline as a core part of your app’s experience, WebView offers the flexibility to customize and update the UI as needed. This is ideal for displaying dynamic content like news articles, interactive guides, or even mini-games without rebuilding them natively.
- Displaying third-party content inline: This includes content such as ads or legal terms and regulations, where you want them to appear as a seamless part of your app’s flow.
- High iteration frequency content: For content that needs frequent updates, such as in-app campaigns or news articles, WebView allows dynamic updates without requiring app releases.
- HTML text editing: WebView can be utilized for HTML-based text editing functionalities within your application.
- Advanced in-app Browse cases: While Custom Tabs are generally recommended for out-of-the-box in-app Browse, WebView can be used if you need a highly customizable Browse experience, for example, if you need to modify the web page’s content or gain deeper analytic insights into user engagement.
- Authentication flows for specific app credentials: Some apps use WebViews to provide sign-in flows using usernames and passkeys/passwords specific to your app, unifying authentication across platforms. However, for third-party identity providers (e.g., “Sign in with…”), Custom Tabs are generally recommended for security.
- Displaying dynamic data that requires an internet connection: If your app displays data that is retrieved from the internet, like emails, it can be more efficient to design a web page tailored for Android and load it in a WebView, rather than performing network requests, parsing data, and rendering it in native layouts.
Getting Started with WebView
To begin using WebView in your Android application, you’ll first need to ensure you have the necessary dependencies.
Adding the Dependency
To leverage WebView effectively, especially for advanced features and compatibility, it's recommended to include the androidx.webkit library. This provides APIs that ensure your app works with the latest WebView features, regardless of the Android version.
dependencies {
implementation("androidx.webkit:webkit:x.x.x")
}Basic XML Usage (Brief Overview)
In traditional Android XML layouts, you would add a WebView directly to your layout file:
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>You would then inflate this layout in your Activity or Fragment and find the WebView, setting its properties and loading a URL programmatically. However, since the focus of modern Android development has shifted towards Jetpack Compose, we will not delve into the detailed usage of WebView with XML layouts in this blog post. Don’t worry though; the core usage of WebView, such as setting its properties and loading URLs, remains largely the same in Compose; only the way the view is laid out differs.
Integrating WebView with Jetpack Compose
In the past, developers often relied on libraries like com.google.accompanist:accompanist-webview to integrate WebView into Compose.
You can read more about it from my older blog.
However, the official documentation for Accompanist WebView, available at Accompanist Web, states that "This library is deprecated, and the API is no longer maintained. We recommend forking the implementation and customising it to your needs." This means you either need to fork and maintain this library yourself or consider using a pre-built solution like compose-webview by KevinnZou, which offers a robust implementation for WebView in Compose. You can examine this further in his article: Using WebView in Jetpack Compose.
For the purpose of this blog, we will mostly focus on a simple implementation of WebView in Compose, demonstrating how to integrate it using the core AndroidView composable and manage its basic functionalities.
Creating a Basic WebView Composable
Here’s how you can create a simple WebView composable that loads a URL. This sample was also shown in the first blog of this series, which is the foundational part, and this is the third blog.
@Composable
fun WebViewCompose(
modifier: Modifier = Modifier,
url: String,
) {
var webView: WebView? = null
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
loadUrl(url)
webView = this
}
}, update = {
webView = it
}
)
}Loading Content and Handling Navigation
The WebView offers various methods to load content:
- Loading a URL: This is the most common use case, loading a webpage from the internet.
webview.loadUrl("http://www.google.com")- Loading HTML data: You can load raw HTML strings directly.
val htmlContent = """
<html>
<body>
<h1 style="color: #FF5722;">Hello, Compose WebView!</h1>
<p style="color: #3F51B5;">This is a sample HTML content displayed inside the WebView.</p>
<ul>
<li style="color: #4CAF50;">Feature 1: Load HTML content</li>
<li style="color: #FF9800;">Feature 2: Customize content dynamically</li>
</ul>
</body>
</html>
""".trimIndent()val mimeType = "text/html"
val encodedHtml = Base64.encodeToString(htmlContent.toByteArray(), Base64.NO_PADDING)
webview.loadData(encodedHtml, mimetype, encoding)or alternatively
val baseUrl: String = "about:blank"
val mimeType = "text/html"
val encoding = "utf-8"
val historyUrl : String?= null
webview.loadDataWithBaseURL(baseUrl, htmlContent, mimeType, encoding, historyUrl)- Loading local files: By placing your HTML content in the app’s assets folder, you can seamlessly load it into a WebView, enabling your application to work offline while keeping all essential content bundled directly within the app.
val assetUrl = "file:///android_asset/$assetFileName"
webview.loadUrl(assetUrl)
Using this option also allows you to localize HTML content for different languages, making your app more accessible to a global audience. Additionally, you can store CSS, JavaScript, and media files in separate folders for better abstraction and maintainability. This keeps your project organized while still allowing the WebView to render everything correctly.
assets/
├── html/
│ └── index.html
├── css/
│ └── style.css
├── js/
│ └── script.js
└── images/
└── logo.pngNote: There are restrictions on what this HTML can do. See
loadData()andloadDataWithBaseURL()for more info about encoding options.
It’s also worth noting that if you only need a simple HTML text to be parsed and displayed, Jetpack Compose now natively supports styling HTML texts using AnnotatedString. Developers should prefer this approach for simpler HTML rendering needs. You can find more details on this in my blog earlier”.
Enable JavaScript
If the web page you want to load in your WebView uses JavaScript, you must explicitly enable JavaScript for it. Once enabled, you can also create interfaces between your app code and your JavaScript code.
By default, JavaScript is disabled in a WebView. You can enable it through the WebSettings attached to your WebView. Retrieve the settings with getSettings(), then enable JavaScript using setJavaScriptEnabled(true).
For example, if you want to load Google Maps, you need to enable JavaScript because Google Maps relies heavily on it to function. Without enabling JavaScript, the map will not render or respond to interactions.
val url = remember { "https://maps.google.com" }
WebView( // where we don't enable javascript
url = url
)Now, let’s enable JavaScript in WebView:
@Composable
fun WebView(
modifier: Modifier = Modifier,
url: String,
) {
var webView: WebView? = null
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
settings.javaScriptEnabled = true
loadUrl(url)
webView = this
}
}, update = {
webView = it
})
}And with this change, we get:
Security Warning
Using addJavascriptInterface() allows JavaScript running inside your WebView to directly call methods in your Android app. While this can be useful, it also introduces potential security vulnerabilities.
If the HTML in the WebView is not fully trusted — for example, if it comes from an unknown source or is partially user-generated — an attacker could inject malicious JavaScript that executes your client-side code or even arbitrary code of their choosing.
Binding JavaScript to Android Code
When building web content specifically for a WebView in your Android app, you can enable two-way communication between JavaScript and native Android code. This allows your web page to trigger Android-specific actions.
To establish this connection, use the addJavascriptInterface() method. This method takes:
- An instance of a Kotlin/Java class containing the methods you want JavaScript to call.
- A string that acts as the interface name in JavaScript.
Once bound, any JavaScript running in the WebView can access the methods in your class via that interface name.
/** Bridge between web content and Android app functionality */
class AppBridge(private val context: Context) {
/** Display a notification message from web content */
@JavascriptInterface
fun displayMessage(message: String) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
/** Get device information accessible to web */
@JavascriptInterface
fun getDeviceInfo(): String {
return "Android ${Build.VERSION.RELEASE}"
}
}In this example, the AppBridge class defines a displayMessage() method and a getDeviceInfo() method. When the web page calls the methods from JavaScript, they execute on the Android side. The displayMessage() method shows a Toast notification and logs the message, while getDeviceInfo() returns device information to the web content.
To bind this class to your WebView, you specify the interface name in the addJavascriptInterface() call:
webView.addJavascriptInterface(AppBridge(this), "NativeApp")Now the web page can call NativeApp.displayMessage("Hello Android!") and NativeApp.getDeviceInfo() from JavaScript, where "NativeApp" is the name you assigned in the binding.
...
<script>
function showCustomToast() {
const message = document.getElementById('messageInput').value;
if (message.trim() === '') {
alert('Please enter a message first!');
return;
}
try {
NativeApp.displayMessage(message);
} catch (error) {
alert('Error: Android interface not available');
}
}
function showQuickToast() {
try {
NativeApp.displayMessage('Quick toast from web!');
} catch (error) {
alert('Error: Android interface not available');
}
}
function getDeviceInfo() {
try {
const deviceInfo = NativeApp.getDeviceInfo();
const infoDiv = document.getElementById('deviceInfo');
infoDiv.innerHTML = '<strong>Device Info:</strong> ' + deviceInfo;
infoDiv.style.display = 'block';
} catch (error) {
alert('Error: Android interface not available');
}
}
</script>
...Note: This HTML demonstrates the core WebView JavaScript bridge functionality. The complete implementation with full CSS styling and additional features can be found in the GitHub repository.
Handling Back Button Presses
A common user expectation in Android apps is that the device’s back button will perform a logical “back” action within the current screen. For a WebView, this means navigating back in its Browse history, not exiting the app or the current screen. If this behavior isn't handled correctly, the user's back gesture will close the activity containing the WebView, leading to a confusing and frustrating experience.
The Unhandled Scenario
Without any specific implementation, the back button press is consumed by the Android system and defaults to navigating back through the application’s activity stack. This means that if the user has navigated to a new page within the WebView, a back press will dismiss the entire screen, ignoring the WebView's own history.
Here is a simple composable that demonstrates this default behavior:
@Composable
fun WebView(
modifier: Modifier = Modifier,
url: String,
) {
var webView: WebView? = null
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val urlToLoad = request?.url?.toString() ?: return false
if (urlToLoad.contains("[your-base-url]")) {
return false
}
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, request.url))
return true
}
}
loadUrl(url)
webView = this
}
}, update = {
webView = it
})
}We implemented this WebViewClient to maintain control over URL navigation within our WebView. Without it, Android’s default behavior might redirect certain links to Chrome Custom Tabs or the system browser, causing users to unexpectedly leave our app. By overriding shouldOverrideUrlLoading(), we can explicitly decide which URLs should remain in the WebView (our internal/base URLs) and which should open externally in the browser, ensuring a consistent user experience and keeping users within our app when intended.
val url = remember { "https://medium.com/" }
WebView(
modifier = Modifier.fillMaxSize(),
url = url
)As you can see in this visual, a back gesture from the user causes the application to exit the screen entirely, instead of navigating back within the web page.
The Handled Scenario
To provide a more intuitive user experience, you can intercept the back button press and first check if the WebView has a history to go back to. This is where Jetpack Compose's BackHandler composable becomes incredibly useful.
BackHandler allows you to define custom behavior for the back gesture. By checking webView.canGoBack(), you can decide whether to navigate the WebView's history or let the system handle the back press (e.g., exiting the screen or the app).
Here’s the updated code that correctly handles the back button press:
@Composable
fun WebView(
modifier: Modifier = Modifier,
url: String,
) {
var backEnabled by remember { mutableStateOf(false) }
var webView: WebView? = null
BackHandler(enabled = backEnabled) {
webView?.goBack()
}
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
...
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
backEnabled = view.canGoBack()
}
...
}
loadUrl(url)
webView = this
}
}, update = {
webView = it
})
}With this implementation, the user’s back gesture now behaves as expected, navigating them to the previous page within the WebView as long as there is a history to go back to.
Conclusion
In this third part of our series, we focused on WebView and its usage within Jetpack Compose. We began with a refresher on what WebView is and when it’s the right choice, particularly for scenarios requiring a high degree of control over web content and for integrating dynamic or frequently updated content. We also explored how to get started with WebView in modern Android development, including the relevant dependencies and a brief overview of its traditional XML usage.
A significant portion of this blog was dedicated to the challenges and best practices of integrating WebView into a Compose UI using the AndroidView composable. We noted the deprecation of the Accompanist library and introduced alternative approaches for a simple implementation. We also discussed various methods for loading content, the importance of handling JavaScript, and the process of binding JavaScript code to native Android code to enable seamless two-way communication. Finally, we demonstrated how to handle back button presses effectively, ensuring a consistent and intuitive user experience.
While we’ve covered the foundational aspects of WebView, there are many more advanced capabilities that you can explore, such as algorithmic darkening and Trusted Web Activities. For a comprehensive overview of all features and detailed documentation, we recommend consulting the official Android developer documentation.
