The Better Android WebViews Collection

Stacy Devino
ProAndroidDev
Published in
9 min readMay 9, 2023

--

WebViews are one of the most controversial subjects in the Community and unfortunately have been much maligned by developers. WebViews can be extremely useful and the best first implementation for a ton of situations from Login and Payments to User Support and Legal. Using WebViews, you can allocate your resources and attention to what your customers value most. WebView-primary apps even make up almost a third of all Apps on the Google Play Store.

So, how do we make really good WebViews?

Knowledge Drop 🧠💧

  1. WebKit implementation varies
    ⏺ Non-Chromium-based (Non-GMS devices and some vendors)
    ⏺ Not all Play-Enabled devices have all features. Use isFeatureSupported and permission checks rather than @Targetan Android OS to be safe.
    ⏺ Customized Chromium Base (Samsung and Amazon Fire devices)
  2. There are 3 “Standard” WebViews available embedded as providers.
    ⏺ “Standalone” AOSP basic implementation and standard for 5.X-6.X
    ⏺ “Monochrome” public and AOSP-accessible unified Browser and WebView for 7.X-9.X
    ⏺ “Trichrome” which is a base Core Library, WebView, and Chrome browser all separated for 10.X — Now (13.X)
  3. Use the modern WebViewCompat extensions with AndroidX Webkit / AndroidX Browser to avoid compatibility issues & version-based errors
  4. All WebView instances in your app will share the same Cache and Dom Storage, improving performance and memory usage
  5. Trust the Chromium Docs first over android.dev
    WebView is now built and maintained by the Chrome team, so their documentation is going to be the most accurate and extensive

WebView Settings for Success 🏆

“Make it look like Chrome” or “Why is Scaling broken?!?!?!”

ReactJS, VueJS and similar front end frameworks rely on viewport and framing information in order to enable their dynamic sizing and styling features. By Default, what exists on Chrome is not what is enabled in the WebView and it requires some browser/web rendering knowledge to even figure out what is going wrong. Try these out to get the flags that Chrome uses by default (And the CLI WebView Shell shares Chrome default flags, which is infuriating when trying to Test a problem!!!).

*This can be KEY for A11Y where Text and Scaling has been enlarged and especially on 720p screens as often users will lose parts of the webpage.

WebView.settings.apply{
layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
loadWithOverviewMode = true
useWideViewPort = true
}

“The WebView page didn’t pass our Security Tests”

You have too much enabled and probably think you need features that you don’t anymore, especially for upload and hybrid interactions
(web <-> native).

  1. Dom Storage — aka Local Storage is the key-value store available to browsers which they sometimes use to manage state, but is also completely unprotected.
  2. Javascript — I encourage you to test if you actually need it for your application, as some pages may not need this. You do need this for Javascript <-> Native app interactions and captures. Otherwise, you could just leaving the ability to execute code here open.
  3. Files — Ton of settings here, but the odds are that unless you are doing something like a PDF or ePub reader they are usually not needed. You absolutely can still do WebView upload with these disabled. If you need to to load local or downloaded assets, use WebViewAssetLoader.
  4. Safebrowsing — Don’t load any ClearText Urls (http://) ever, just… no
WebView.settings.apply{
domStorageEnabled = false //Careful here as your Website might need this
javaScriptEnabled = false //Check if you actually need this
allowContentAccess = false
allowFileAccess = false
allowFileAccessFromFileURLs = false
allowUniversalAccessFromFileURLs = false
safeBrowsingEnabled = true //API 26+ only, use the Compat version below!
}

if(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLED)){
WebSettingsCompat.setSafeBrowsingEnabled(WebView.settings, true)
}

BONUS: Did you know that shouldOverrideUrlLoading() only works on get requests? So, its up to do you to your own validation before loadUrl too.

“Why is this scroll not 60FPS tho?”

  1. Optimizing your Web Code will do more than optimizing your WebView
    ⏺ Minify Code
    ⏺ Strip your CSS and packages to what you just need for those pages
    ⏺ Image services that scale to the device/window being serviced
  2. More Flags for your profit
WebView.settings.apply{
loadsImagesAutomatically = true //default value but should declare too
offscreenPreRaster = true //only API 23+, use Compat version below!
setSupportZoom(false) //disable to load scale to window size, but disables zoom :(
}

if(WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)){
WebSettingsCompat.setOffscreenPreRaster(WebView.settings, true)
}

3. Think about your Caching Strategy (LOAD_DEFAULT, LOAD_NO_CACHE, LOAD_CACHE_ELSE_NETWORK, LOAD_CACHE_ONLY) and what the tradeoffs are for your implementation. Cache is powerful.

WebView.settings.setCacheMode(WebSettings.LOAD_DEFAULT)

Testing Your WebViews 🧪

“The Web Team needs something that they can test with on CLI”

Introducing System WebView Shell which you can build yourself (with your own settings) or use one of the pre-built APKs available HERE.

This is the package used by the Compatibility Test Suite and should be a good baseline. Remember though, by default it comes with the WebView.Settings that are used by Chrome and not all the default values assigned in AOSP base.

Some good instructions on gathering traces for reporting are found for Instrumented Tests here and Logging of network data here.

“How do I test it in the App with Tools?”

You can use Chrome Dev Tools with your Android App WebViews.

There are some limitations though not present in usual Chrome.
— You cannot change executing code in the window (edits mean nothing)
— Code execution breakpoints work, but might be more flaky for inspection
— User Gestures and object Inspection don’t really work through the view port

  1. Make your WebViews Debuggable (only valid API 19+)
    *Be smart 🧐 and put this in your debug app build setting parameters.
WebView.setWebContentsDebuggingEnabled(true)

Now, you can run your app and connect to that active WebView by going to chrome://inspect/#devices in Chrome and tapping the inspect button under your device. This works for Emulators and Real Devices on the same local network.

2. Network Settings to make it work for ClearText urls and Proxying
*Odds are if you are testing a web application routed through local host or another port locally, you will need to enable insecure URLs or Proxying. The default setting for Android is to have ClearText disabled in all cases.

Add this to your network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Debug Override considerations -->
<debug-overrides cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>

3. Add the AndroidX Tracing Controller for tracing deeper into the code at runtime and performance profiling

if (WebViewFeature.isFeatureSupported(
WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) {
val tracingController = TracingController.getInstance()
tracingController.start(
TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build()
)
}
....
//Optional
tracingController.stop(FileOutputStream("trace.json"), Executors.newSingleThreadExecutor())

“Unit Test all the things!”

Some of the Best Instructions and Samples on how to do this well again live in the Docs.

Take a Look at the AOSP Tests as well as the SourceCode for AndroidX Browser and Webkit.

Hybrid Apps

“My WebViews need to talk to my App and my App needs to talk to my WebViews” 👉👈

Time to skip and stop using the ol’ JavascriptInterface standard for Web <-> App communication and start using the HTML5 MessageChannel spec because we care about user security and message integrity! This spec works on iOS too.

This is a dedicated set of ports for doing posting and receiving of WebMessage. For obvious reasons, please use WebMessageCompat to avoid compatibility issues.

val uri = Uri.parse("https://developer.android.com/develop/ui/views/layout/webapps")
// Check if this feature is supported,
// Don't use Android API versions as they aren't guaranteed
if (WebViewFeature.isFeatureSupported(
WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)){
createMessageChannel()
}

fun createMessageChannel(){
val channel = webView.createMessageChannel() //Creates 2 ports
val recievePort = channel[0]
val sendPort = channel[1]
//Receive on port 0
recievePort.setWebMessageCallback(
WebMessagePortCompat.WebMessageCallBackCompat(){
override fun onMessage(port: WebMessagePortCompat,
message: WebMessageCompat?){
// Received Message
}
})
//Send on port 1
webView.postWebMessage(WebMessageCompat("a message",
WebMessagePortCompat[]{sendPort}, uri))
}

Some Caveats to be aware of:

  1. This only works on Loaded Pages, so no “startup” code can be gathered here. Must wait for onPageFinished() to be called inside of WebViewClientCompat
  2. Android 6.0+ only, not supported on older.
  3. file://asset_url doesn’t really work here, so if that is your content (ePub Readers and PDF loaders for example) use webview.loadDataWithBaseUrl() instead of loadUrl().

“I need my web page to only load the Android || iOS version”

Did you know that you can grab Javascript Boolean parameters from Android AND iOS (WKWebView) PRIOR TO THE onDocumentReady() CYCLE IN JAVASCRIPT?!

No really, and I have done it to build ALL KINDS of WebViews that “Feel Native” and it’s a solid alternative to custom URL parameters.

Basically, you need to do a Javascript query against the window or a custom extension ala window.mobileInterface for the JS Object. Then just assign boolean values to your JavascriptInterface function that maps to in Android 👇.

class MyJSInterface(){
@JavascriptInterface
fun isAndroid():Boolean { return true }
}

val extJSConst = "window.mobileInterface"

And then just add WebView.addJavascriptInterface(MyJSInterface, extJSConst)

which means that window?.mobileInterface.isAndroid() from Javascript would returntrue || null with null being the fail-over.

Since that can be executed prior to onDocumentReady , it means you can set objects to render and not to render in your JS code. This means that the end user will never see a flashing screen of things turning off, execution is faster and streamlined, and you can customize your “Mobile-first” experience without messing with your other web code.

In my opinion, this is just about the only acceptable use of @JavascriptInterface in modern apps because manipulation through MITM security or data concerns just simply don’t exist.

“I need my WebViews to be logged in.” / “Why are my users getting logged out?”

We need to use the CookieManager. This is because most login sessions for Web Apps use Cookies that contain their Token to manage their session validity.

Cookies are managed by default in the caching scheme you defined earlier and that is self managing. If your login is also a WebView (you should consider using a Chrome Custom Tab if you are a hybrid app), the WebView should receive the cookie and “it just works” with regards to your WebViews. If not, you will need to grab your Logged-In token and generate a Cookie or have a Cookie Generator API in your system which you then set below

val cookieManager = CookieManager.getInstance()
cookieManager.setCookie(myUrl, myCookieString, null) //Can use ValueCallback instead of null

This automatic web and cache management does mean that when the user logged in session has timed out either from an invalid session or an expired cookie, it will kick the user out to log in again. This does not align with mobile user expectations where the user has logged in and the app remembers in-perpetuity. This is why users might be complaining about being “logged out” and more importantly for a business, could be negatively affecting your notifications and metrics.

So, let’s save our Logged-In token and store our existing Cookies to put back when the app is restarted. Or, just the cookies we care about most. Now, how you save and store this is very much going to depend on your application and I would encourage you to look at options like making an encrypted DataStore or type-safe Proto DataStore. Don’t worry, invalidation on the web side will still work correctly if you have a user return outside of the window or if you had to do an internal rotation.

val cookieManager = CookieManager.getInstance()
val cookiesString = cookieManager.getCookies()
//Store cookies how you see fit....
cookieManager.setCookie(myUrl, cookiesString, null)

Conclusion

Not everything needs to or should be fully-Native Mobile. We can build great web experiences in our Apps for our users when we put in the work.

So, I encourage you to think deeply about whether those “Terms and Conditions” popups should be accessed and maintained in an API or simply route to your Url? Do you really want to build all of your customer support pages natively as well? Is this a good use of our resources to build this out natively? If it isn’t for now, doesn’t this still give us a path to evolve later?

Using WebViews effectively can enable teams to move quickly and spend time on what matters most to their customers. Building better Web Apps and WebViews means building better mobile-first experiences that can grow and evolve as you and your company does. Sometimes, web-first is the only solution to getting your product in front of customers and that is fine too. Let’s just make them the best we can! 🥇

SPECIAL THANKS!
I want to thank the Chrome teams for all of the effort and great resources that they have built to help us in our journey to higher quality end user experiences from one engineer to another.

Editors and commenters: Erik Hellman, Mike Nakhimovich, Mike Wolfson

--

--

Mistress of Android, Diversity Advocate, Speaker, and Builder of Communities. @DoesitPew on Twitter. @childofthehorn on Github. @Stacy@androiddev.social.