
Extracting HTML properties from an Android Web View in Jetpack Compose
You can now find this and other posts over on my substack page at https://thatgreekengineer.substack.com/ !
I was recently tasked with the implementation of a 3DS payment verification flow for one of our new projects, and I had to pass various HTML properties to our backend in order to initialize the process. These properties include things such as Navigator.language, Navigator.javaEnabled, Screen.ColorDepth and many more. This begs the question, how can we retrieve such HTML properties through an Android WebView dynamically? Well, the answer is… JavaScript interfaces. Let’s see how to do that in a Jetpack Compose screen.

1. Adding the WebView
First and foremost, let’s start by creating a separate @Composable
for our WebView screen.
The only thing that we need to do, is to use the AndroidView
composable which will compose an Android view from the given factory method, which in this case will be the WebView. That’s pretty much it! I’ve also added a simple BackHandler
snippet to help you handle back navigation while inside the WebView.
@Composable
fun Payments3DSWebViewScreen(
navController: NavController
) {
val context = LocalContext.current
var webView by remember { mutableStateOf<WebView?>(null)
Column(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = {
WebView(context).apply {
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
webView = this
}
},
modifier = Modifier
.fillMaxSize()
.background(Color.White)
)
}
BackHandler {
if (webView != null) {
if (webView!!.canGoBack()) {
webView?.goBack()
} else {
navController.popBackStack()
}
} else {
navController.popBackStack()
}
}
}
2. Enabling JavaScript in our WebView
This is very simple, and it only requires that you add the settings.javaScriptEnabled = true
line inside the WebView
block like so:
WebView(context).apply {
settings.javaScriptEnabled = true
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
webView = this
}
That’s it, now that JavaScript has been enabled, we can start interfacing with it.
Quick reminder: You should strongly consider if you need to enable JavaScript in your application since it can lead to security issues. So do your research and act according to your project’s needs.
3. JavaScript interfaces in Android
So what’s a JS interface? It’s basically a function within your android app that can be called inside the Web View as a JS script. This means that since it will be run inside a JS script, you can access most properties that would be accessible by such a script in a browser. These functions must be annotated with @JavascriptInterface
! Here’s an example of such an interface method from my app:
class PaymentsJSInterface() {
lateinit var language: String
var javaEnabled: Boolean = false
lateinit var screenColorDepth: String
lateinit var screenHeight: String
lateinit var screenWidth: String
lateinit var timezoneOffset: String
lateinit var userAgent: String
@JavascriptInterface
fun getNavigatorInfo(
lang: String,
javaEnabled: Boolean,
screenColorDepth: String,
screenHeight: String,
screenWidth: String,
tzOffset: String,
userAgent: String
) {
language = lang
this.javaEnabled = javaEnabled
this.screenColorDepth = screenColorDepth
this.screenHeight = screenHeight
this.screenWidth = screenWidth
timezoneOffset = tzOffset
this.userAgent = userAgent
Log.d(
"PAYMENTJS",
"$lang | $javaEnabled | $screenColorDepth | $screenHeight | $screenWidth | $tzOffset | $userAgent"
)
}
}
As you can see, I’ve added the function inside a separate class, which will be needed in the next steps. You will also notice that it accepts a few parameters that will be set from our custom JS code that will be run inside the Web View.
4. Using our JS interface within the web view
Alright, so now that we have our interface method set up, let’s connect it with our Web View. This is achieved in two steps:
First, we need to call theaddJavascriptInterface(paymentsJSInterface, "APP")
inside our WebView block, which, as you can tell, requires an instance of the class containing the method that we previously annotated with the @JavascriptInterface
and a “namespace” that will be used to specify which function from that namespace (namely, our app), JS should run. This can be any string that you like.
The second step is adding a call to the loadUrl()
method and specifying that instead of a URL, we want it to load a custom JS script that will call a JavaScriptInterface method from the namespace that we set earlier. This call should also be placed inside the WebView block. Here’s a complete example:
val paymentsJSInterface by remember { mutableStateOf(PaymentsJSInterface()) }
AndroidView(
factory = {
WebView(context).apply {
settings.javaScriptEnabled = true
addJavascriptInterface(paymentsJSInterface, "APP")
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
webView = this
webView?.loadUrl("javascript: APP.getNavigatorInfo(navigator.language || navigator.userLanguage, navigator.javaEnabled(), screen.colorDepth, screen.height, screen.width, new Date().getTimezoneOffset(),navigator.userAgent);")
}
},
modifier = Modifier
.fillMaxSize()
.background(Color.White)
)
As you can see here, I’ve set my app’s namespace to “APP” for this example. Now, inside the loadUrl
method, we specify that this is a custom JS script by beginning the “URL” with javascript:
and then calling our getNavigatorInfo()
method through our APP
namespace.
You will also notice that we now have full access to any JS-related object inside this custom JS script. So for this method, I can simply access the browser’s language through navigator.language
or the user’s screen color depth through screen.colorDepth
.
5. Seeing everything in action
Alright, now that everything is ready, let’s run the app! Once the WebView screen comes to the foreground, the loadUrl
method will execute our custom JS script, thus calling the getNavigatorInfo()
method, which should then log a few of the necessary JS/HTML properties.
Aaaand….

6. La fin
You’ve made it, congratulations! Now you can use this knowledge and do even greater things, such as passing these fields to your ViewModel for further analysis or redirecting/authenticating users based on the data!