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 POST
ing 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:
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.
- On iOS every page load if caught by webView(_:decidePolicyFor:decisionHandler:) and can be treated by our code
- Except
target="_blank"
links which have to be handled by webView(_:createWebViewWith:for:windowFeatures:) which is actually another delegate!
- Except
- On Android, only a subset of page loads are handled by shouldOverrideUrlLoading()
- The first URL loading programmatically is always loaded
- Only
GET
loads go through the filter. WhenPOST
ing a form, the handler is not called
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.