featured.png

It's never "just a WebView"

  • Jonas Schmid

We get requests from clients who already have a website and "just" want to see it in an app regularly. While it's often not a good choice business-wise, it also has technical challenges.

Thomas already talked about how the web and the app ecosystems are different. They don't have the same goals, and should aim for a different user experience. I will focus on the technical side on implementing a website into a native app using WebView on Android and WKWebView on iOS here.

Websites have extra UI that you don't want in your app

Websites always have extra content which is not needed, when wrapping them in an app. They have a header title, a navigation menu, and a footer with extra links. Depending on how much "native" you want your app to appear, you will show a native navigation bar, a custom navigation flow and certainly not a footer under each screen.

If you are lucky, you can ask the website developer to create a special template for the app to remove those extra features and only show the main content. Otherwise you'll have to inject javascript to hide this content.

If the website contains a special "app" template, make sure you always use it

We built an app where the website had a special template. Each page could be loaded with the extra parameter mobile=true like http://www.liip.ch/?mobile=true. This worked great, but each link contained to the page did not have this extra parameter. If we’d simply allowed link clicks without filtering, the user would see the non-app pages. We had to catch every link that the user clicked, and append the extra parameter "manually". This is quite easy for GET parameters, but it can get quite tricky when POSTing a form.

Making sure users cannot go anywhere

By default a WebView will follow every link. This means that as long as the web page shows a link, the user will be able to click on. If your page links to Google or Wikipedia, the user can go anywhere from within the app. That can be confusing.

It is easier to block every link, and specifically allow those that we know, in a "whitelist" fashion. This is particularly important because the webpage can change without the app developer being notified. New links can appear on the application and capsize the whole navigation.

WebViews take a lot of memory

WebViews use a lot of RAM compared to a native view. When a WebView is hidden — when we show another screen or put the app in background for example — it is very likely that the system will kill the Android Activity that contains the WebView or the whole iOS application.

As it takes a lot of memory, it matches killing criterias to making space for other apps in the system.

When restoring the Activity or application which contains the WebView, the view has lost its context. This means, if the user entered content in a form, everything is gone. Furthermore, if the user navigated within the website, the WebView can’t remember on which page the user was.

You can mitigate some of the inconvenience by handling state restoration on Android and iOS. Going as far as remembering the state inside a WebView will cause a lot of distress for the developer :)

WebView content is a blackbox

Unless you inject JavaScript to know what is inside, you cannot know what is displayed to the user. You don't know which areas are clickable, you don't know (easily) if the view is scrollable, etc...

WebViews don't handle file downloads

On iOS, there is no real concept of file system, even with the new Files app. If your website offers PDF downloads for example, the WebView does simply nothing. An option is to catch the URLs that are PDFs and open Safari to view them.

On Android, you can use setDownloadListener to be notified when the WebView detects a file that should be downloaded instead of displayed. You have to handle the file download by using DownloadManager for example.

webview.setDownloadListener { url, _, contentDisposition, mimetype, _ ->
  val uri = Uri.parse(url)
  val request = DownloadManager.Request(uri)
  val filename = URLUtil.guessFileName(url, contentDisposition, mimetype)
  request.allowScanningByMediaScanner()
  request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
  request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
  val dm = context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
  dm.enqueue(request)
}

WebViews don't handle non-http protocols such as mailto:

You will have to catch any non-http protocol loads and define what the application should do.

The Android back button is not handled

If the user presses their back button, the standard action (leave activity, ...) will happen. It will not do a "previous page" action like the user is used to. You have to handle it yourself.

webview.setOnKeyListener { _, keyCode, event ->
 if (keyCode == KeyEvent.KEYCODE_BACK && event.action == MotionEvent.ACTION_UP && webview.canGoBack()) {
    webview.goBack()
    return@setOnKeyListener true
 }
  return@setOnKeyListener false
}

There is no browser UI to handle previous page, reload, loading spinner, etc...

If the website is more than one page, you need to have the possibility to go back and forth in the navigation history. This is important if you removed the website's header/footer/menu in particular. On Android, users can still use the back button (if you enabled it) but on iOS there is no way to do that.

You should offer the possibility to reload the page. Showing a loading indicator so that users know when the page is loaded completely, like in a native application or a standard browser helps.

This is no default way to display errors

When an HTTP error occurs, if you don't have a network connection for example, the WebView doesn’t handle it for you.

On iOS, the view will not change. If the first page fails to load, you will have a white screen.

On Android, you will have an ugly default error message like this one:

Sample Android WebView error

WebViews don't know how to open target="_blank" links

WebViews don't know how to handle links that are supposed to open a new browser window. You have to handle it. You can decide to stop the "new window" opening and load the page on the same WebView for example. But by default nothing will happen for those links.

iOS and security

iOS is — rightfully — very conservative regarding security. Apple added App Transport Security in iOS 9 to prevent loading unsecure content. If your website does not use a recent SSL certificate, you will have to disable ATS, which never is a good sign.

It is hard to make code generic

Since every page can be different, having different needs, it is hard to make code generic. For a client, where we show two sub-pages of their website on different screens, we have two webview handlers because of the various needs.

Once you have thought through all these things and believe you can start coding your webview, you will discover that iOS and Android handle them differently.

There is a strong dependency on the website

Once you faced all challenges and your app is ready to ship, there is one last thing that you cannot control: You are displaying a website that you did not code, and that is most likely not managed by someone in your company.

If the website changes, there is a high chance that the webmaster doesn’t think of telling you. Mainly because he doesn’t think one change on the website is enough to mess up your app completely.

Conclusion

WebViews can be used for wrapping upon existing websites fast, but it is never "just" a WebView. There is some work to do to deliver the high quality that we promise at Liip. My advices for you:

  • Use Defensive programming.
  • List all features of each page with the client, and define the steps clearly, like for any native app.
  • Get in touch with the website developer and collaborate. Make sure they tell you when the website changes.

Tell us what you think