WebViewAssetLoader and WebViewClient with Kotlin

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    May. 10, 2021
    Last Updated:
    May. 10, 2021
  • classTags
    classClick for Tags

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

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.

  1. Recommend: Use Android Studio: Project Structure -> Dependencies -> Add Dependency (+) -> Library Dependency and then search for and add androidx.webkit
  2. 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 fromfile:///android_asset/... to https://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.

Leave a Reply

Your email address will not be published. Required fields are marked *