Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android and the WebView

Android and the WebView

While the native widgets for Android are extremely powerful and cover most of the use cases we can imagine, there are still some areas where web content is our only option. That means we need to embed a WebView in our application and create a bridge between the native code and the JavaScript running on the web page we load.

In this session you will learn everything there is about how to deal with the WebView on Android. Learn how to properly load content, inject your own CSS or JavaScript, how to effectively communicate between JavaScript and Android, and how to make loading web content secure in Android applications. We will also look at how to implement some common use cases, like displaying ePub and other HTML-based e-books.

2307a37297162f815342545a2068b2f1?s=128

Erik Hellman

July 02, 2019
Tweet

Transcript

  1. Android and the WebView @ErikHellman speakerdeck.com/erikhellman/android-and-the-webview

  2. If all you have is a hammer, everything looks like

    a nail!
  3. What else would you really need?!? <WebView id="@+id/webView" layout_height="match_parent" layout_width="match_parent">

    val webView = findViewById(R.id.webView) webView.loadUrl("https://dailykitten.com")
  4. None
  5. Don't use the WebView! <!-- TODO: REMOVE THIS --> <WebView

    id="@+id/webView" layout_height="match_parent" layout_width="match_parent">
  6. Use Chrome Custom Tabs instead!

  7. Chrome Custom Tabs Gradle: implementation 'androidx.browser:browser:x.y.z' Kotlin: val url =

    Uri.parse("https://dailykitten.com") CustomTabsIntent.Builder().build().launchUrl(this, url)
  8. Customizing toolbar color val url = Uri.parse("https://dailykitten.com") CustomTabsIntent.Builder() .setToolbarColor(R.color.mainCoonGrey) .build().launchUrl(this,

    url)
  9. Custom action button val icon = R.drawable.cat val description =

    R.string.send_to_cat val pendingIntent = PendingIntent.getActivity(...) val tint = true CustomTabsIntent.Builder() .setActionButton(icon, description, pendingIntent, tint) .build().launchUrl(this, url)
  10. Customizing transitions CustomTabsIntent.Builder() .setStartAnimations(this, R.anim.fancy_in_anim, R.anim.fancy_out_anim) .setExitAnimations(this, R.anim.fancy_out_anim, R.anim.fancy_in_anim) .build().launchUrl(this,

    url)
  11. Launch pages faster val connection = object : CustomTabsServiceConnection() {

    override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { client.warmup(0) client.newSession(CustomTabsCallback()) .mayLaunchUrl(Uri.parse("https://dailykitten.com"), Bundle.EMPTY, emptyList()) } override fun onServiceDisconnected(name: ComponentName) { } } CustomTabsClient.bindCustomTabsService(this, "com.android.chrome", connection)
  12. android.webkit.WebView

  13. Avoid WebView if possible!

  14. When NOT to use WebView1 — Displaying SVG images —

    Login dialogs (Except when you must) — Wrapping your mobile website in an APK 1 Not a complete list...
  15. When to use WebView2 — Displaying local HTML formatted content

    — Web-only authentication 2 Mostly complete list.
  16. None
  17. None
  18. Getting Started 1. Enable Chrome Dev tools // Enable Chrome

    Dev Tools with chrome://inspect WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
  19. Getting Started 1.2 Chrome Dev Tools (chrome://inspect)

  20. Getting Started 1.3 Chrome Dev Tools

  21. Getting Started 2. Add compat library dependencies { ... implementation

    'androidx.webkit:webkit:x.y.z' ... }
  22. Getting Started 3. Configure your WebView val webView = findViewById<WebView>(R.id.webView)

    webView.settings.apply { ... }
  23. Getting Started 3.1 Basic settings // Enable if your content

    uses JavaScript javaScriptEnabled = true // For localStorage or sessionStorage domStorageEnabled = true // Web Database API support (IndexedDB) databaseEnabled = true // true if the WebView supports the viewport meta tag useWideViewPort = true // Support zooming displayZoomControls = false builtInZoomControls = false
  24. Getting Started 3.2 Caching // Overrides the way the cache

    is used cacheMode = WebSettings.LOAD_NO_CACHE
  25. Getting Started 3.3 File and content URLs // How to

    support file:/// URLs (Don't!) allowFileAccess = false allowFileAccessFromFileURLs = false allowUniversalAccessFromFileURLs = false // Allow access to ContentProvider URLs allowContentAccess = false
  26. Getting Started 4.1 Load your remote content3 val webView =

    findViewById<WebView>(R.id.webView) val url = "https://api.catz.com/news/content/12345" webView.loadUrl(url) 3 Don't do this - see later slides.
  27. Getting Started 4.2 Load generated content val html = """<html>

    <head> <title>Cats!</title></head> <body> <h1>Cats are awesome!</h1> </body> </html>""" val htmlBytes = html.toByteArray(charset("UTF-8")) val encodedHtml = Base64.encodeToString(htmlBytes, Base64.NO_WRAP) webView.loadData(encodedHtml, "text/html", "base64")
  28. Getting Started 4.3 Loading local content (the right way!) val

    url = "https://catz.androidplatform.net/index.html" val webView = findViewById<WebView>(R.id.webView) webView.loadUrl(url)
  29. WebViewClient

  30. WebViewClient

  31. WebViewClient

  32. WebViewClient usage — Intercept WebView requests — Override network response

    with custom content — Always use WebViewClientCompat!
  33. shouldOverrideUrlLoading() — Only for GET requests — Don't call WebView.loadUrl()

    or WebView.loadData() here — Useful for capturing auth tokens
  34. Capturing OAuth 2 auth code val webView = findViewById<WebView>(R.id.webView) webView.webViewClient

    = object : WebViewClientCompat() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { if (request.url.toString().startsWith(REDIRECT_URL)) { return handleLoginSuccess(request) } return false } } val loginUrl = ... // Login URL with redirect URL webView.loadUrl(loginUrl)
  35. androidplatform.net One potential problem of hosting local resources on a

    http(s):// URL is that doing so may conflict with a real website... The androidplatform.net domain has been specifically reserved for this purpose and you are free to use it. — https://github.com/google/webview-local-server
  36. Loading local content safely webView.webViewClient = object : WebViewClientCompat() {

    override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { // Read local content and return a WebResourceResponse return createResponse(request) } } webView.loadUrl("https://catz.androidplatform.net/index.html)
  37. Loading local content safely fun createResponse(request: WebResourceRequest): WebResourceResponse { val

    path = request.url.path val file = fileFromUrlPath(path) if (!file.exists()) { return WebResourceResponse("text/plain", "utf-8", 404, "Not Found", emptyMap(), ByteArrayInputStream(ByteArray(0))) } val (mimeType, encoding) = mimeTypeAndEncodingForFile(file) return WebResourceResponse(mimeType, encoding, 200, "OK", emptyMap(), FileInputStream(file)) }
  38. WebViewServer.kt - http://bit.ly/2xkYMGD open class FileRequestHandler(file: File, private val requestPath:

    String, private val mimeType: String, private val encoding: String) : RequestHandler { private val bytes = FileInputStream(file).use { it.readBytes() } override fun shouldHandleRequest(request: Request): Boolean { val uri = Uri.parse(requestPath) return uri.path == request.path } override fun handleRequest(request: Request): Response { return Response(200, "OK", emptyMap(), mimeType, encoding, ByteArrayInputStream(bytes)) } }
  39. Bridging Android and JavaScript

  40. Bridging Android and JavaScript (old version) class MyJavaScriptInterface(private val handler:

    Handler) { fun onEvent(data: String) { val json = JSONObject(data) Message.obtain(handler, JS_MSG, json).sendToTarget() } companion object { const val JS_MSG = 10 } }
  41. Bridging Android and JavaScript (old version) val webView = findViewById(R.id.webView)

    val jsInterface = MyJavaScriptInterface(jsHandler) webView.addJavascriptInterface(jsInterface, "android")
  42. Bridging Android and JavaScript (old version) function emitEventToAndroid(data) { if

    (android && typeof android.onEvent === 'function') { android.onEvent(JSON.stringify(data)); } }
  43. Bridging Android and JavaScript (old version) // JavaScript function we

    will call from Android function performAction(name, age) { return `${name} is ${age} years old!` } val name = Erik val age = 42 webView.evaluateJavascript("performAction($name, $age)") { value -> // value will be "Erik is 42 years old!" }
  44. Bridging Android and JavaScript (old version) — Threading — Binding

    native objects — Calling function by strings
  45. Better solution: MessageChannel!

  46. Bridging Android and JavaScript (new version) // Creates two MessageChannel

    ports val (port1, port2) = WebViewCompat.createWebMessageChannel(webView) port1.setWebMessageCallback(object: WebMessagePortCompat.WebMessageCallbackCompat() { override fun onMessage(port: WebMessagePortCompat, message: WebMessageCompat?) { message?.data?.also{ val data = JSONObject(it) handleWebEvent(data); } } }) // Send init message to JS side val initMsg = WebMessageCompat("""{type: "init"}""", arrayOf(port2)) WebViewCompat.postWebMessage(webView, initMsg, Uri.parse("*"))
  47. Bridging Android and JavaScript (new version) let nativePort = null;

    window.addEventListener('message', message => { if (e.data) { val msg = JSON.parse(e.data); if (msg.type === 'init') { nativePort = e.ports[0]; } else { onNativeMessage(msg); } } }); function sendMessageToNative(data) { nativePort.postMessage(JSON.stringify(data)); }
  48. Customizing local web content

  49. ePub standard

  50. ePub in 1 slide — W3C standards (2.0, 3.x) —

    ZIP file (or directory) structure — Self-contained web content — Requires custom JS and CSS for rendering
  51. ePub structure

  52. ePub structure

  53. Package document <?xml version="1.0" encoding="UTF-8" ?> <package version="2.0" unique-identifier="PrimaryID" ...>

    <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" ...> <dc:title>The Dark World</dc:title> ... </metadata> <manifest> <!-- All content in this book --> <item id="cover" href="cover.xml" media-type="application/xhtml+xml"/> <item id="book-cover" href="images/cover.png" media-type="image/png"/> <item id="page-css" href="css/page.css" media-type="text/css"/> ... </manifest> <spine toc="ncx"> <!-- The order of all chapers --> <itemref idref="cover" linear="yes"/> <itemref idref="titlepage" linear="yes"/> <itemref idref="about" linear="yes"/> <itemref idref="main0" linear="yes"/> ... </spine> ... </package>
  54. ePub scrolling

  55. Wrapping web content (HTML)

  56. Wrapping web content (HTML) <html> <head> <title>ePub Reader</title> <link rel="stylesheet"

    href="book.css" type="text/css" /> <script src="epub-reader.js"></script> </head> <body> <div id="resourceRoot"> <!-- Add iframes here --> </div> </body> </html>
  57. Injecting CSS fun loadEpub(spineItems) { const resourceRoot = document.querySelector('#resourceRoot'); spineItems.forEach(item

    => { const iframe = document.createElement('iframe'); iframe.addEventListener('load', () => { const cssLink = document.createElement('link'); cssLink.rel = 'stylesheet'; cssLink.type = 'text/css'; cssLink.addEventListener('load', () => { console.log('Chapter loaded with custom CSS!'); }); cssLink.href = '/chapter.css' // Add custom CSS to this chapter const head = iframe.contentDocument.querySelector('head') head.appendChild(cssLink); }); resourceRoot.appendChild(iframe); // Start loading chapter in iframe iframe.src = item.href; }); }
  58. Injected CSS :root { column-gap: 20px; column-width: 45em; column-count: 1;

    column-fill: auto; will-change: transform; } body { overflow: hidden !important; column-span: none !important; box-sizing: border-box !important; break-inside: avoid !important; }
  59. Demo!

  60. Dos and Donts 1. Don't run a local web server

    for local web content 2. Don't scroll web content from native 3. Avoid multiple WebViews 4. Use MessageChannel for bridging 5. Use WebViewClientCompat for loading local content 6. WebView is auto-updated with Chrome (except on Android 5!)
  61. WebView resources - Native Bridge Android A lightweight and efficient

    bridge between webview and native apps (Android & iOS). github.com/nrkno/nativebridge-android - WebViewServer.kt A customizable "server" for Android WebView: http://bit.ly/2xkYMGD - WebView Local Server from Google A simple implementation for loading local content.
  62. Thank you for listening! @ErikHellman speakerdeck.com/erikhellman/android-and-the-webview