Intro
For fun, I’m trying to hack together a little Android app for myself. Because it potentially will use low-level system calls, I wanted to try my hand at building the app with a native Android approach – Kotlin, Java, and Android APIs – as opposed to React Native or Flutter. This is a considerable challenge to take on, given that up to this point I have had zero experience with Java, Kotlin, or native Android development.
This post is about a particular challenge I ran into – trying to use WebViewAssetLoader
and WebViewClient
to get around the ERR_ACCESS_DENIED
issue with local HTML files.
Issue
My scenario is fairly standard – I have some local HTML files that I want to load into a WebView
. The first attempt, loading via file:///android_asset/my-file.html
failed with ERR_ACCESS_DENIED
.
At first glance, the easy way to solve this seems to be through one or more of the following (depending on API level) (based on these responses):
setAllowFileAccess(true)
- Adding
android:usesCleartextTraffic="true"
to the manifest
However, after further digging, it became clear that both of the above options are not best practice, and actually reduce the security of our app. The better approach, which is mentioned in the setAllowFileAccess
docs, is to use androidx.webkit.WebViewAssetLoader
.
However, the docs for that class only show examples in Java, even when you select Kotlin
as the language. Given that I had no experience with Java, and they were using more advanced syntax, this is where I got stuck for a tiny bit (and is the reason for this post).
Solution
Installing the WebViewAssetLoader Dependency
First, before even working on the Kotlin code, something that I had to figure out was that WebViewAssetLoader is an extra dependency – it is part of androidx
not android
. Note the import path:
import androidx.webkit.WebViewAssetLoader
So, before moving on, go ahead and install the required dependency. You can do this in one of two ways.
- Recommend: Use Android Studio:
Project Structure -> Dependencies -> Add Dependency (+) -> Library Dependency
and then search for and addandroidx.webkit
- Alternatively, you can add the dependency directly to your
gradle
file. For example:implementation 'androidx.webkit:webkit:1.4.0'
WebViewAssetLoader + WebViewClient
OK, so now that we have the dependency installed, how do we go about actually using WebViewAssetLoader
with a webview? In trying to re-build the example from the docs, I got tripped up on some fancy syntax and how to emulate it in Kotlin:
// Java code
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return assetLoader.shouldInterceptRequest(request.getUrl());
}
// ...
}
Coming from web development, that syntax for both extending and initializing a class instance in the same section (and as an argument!) is something I had rarely ever seen before. In Java, this is known as Anonymous Class
syntax.
To replicate this same syntax in Kotlin, there are several options, but the important part is mostly the object : MyClass() { }
syntax..
Here is a consolidated example, showing an activity that uses WebView with a local HTML file, via WebViewAssetLoader:
MyActivity.kt
:
package com.joshuatz.demo
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewAssetLoader
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_activity);
val webView: WebView = findViewById(R.id.myWebView);
// Setup asset loader to handle local asset paths
val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(this))
.build();
// Override WebView client, and if request is to local file, intercept and serve local
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(request.url);
}
}
webView.loadUrl("https://appassets.androidplatform.net/assets/my-file.html");
}
}
💡 There is another important change between using WebViewAssetLoader and standard WebView – notice how the path changed from
file:///android_asset/...
tohttps://appassets.androidplatform.net/assets/...
. The new domain is less important (it’s a non-existent subdomain), but the/assets/
is very important for our WebViewAssetLoader path handler – it is how it matches the URL.
Tenía JUSTO este problema. Se supone que está deprecated desde hace más de un año, sin embargo, aún Google no muestra nada de cómo solucionar esto en Kotlin. Muchísmas gracias!
Perfect example. This should be the official documentation. Thanks for posting!