$30 off During Our Annual Pro Sale. View Details »

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.

Erik Hellman

July 02, 2019
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. What else would you really need?!?
    layout_height="match_parent"
    layout_width="match_parent">
    val webView = findViewById(R.id.webView)
    webView.loadUrl("https://dailykitten.com")

    View Slide

  4. View Slide

  5. Don't use the WebView!

    layout_height="match_parent"
    layout_width="match_parent">

    View Slide

  6. Use Chrome Custom Tabs instead!

    View Slide

  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)

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  12. android.webkit.WebView

    View Slide

  13. Avoid WebView if possible!

    View Slide

  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...

    View Slide

  15. When to use WebView2
    — Displaying local HTML formatted content
    — Web-only authentication
    2 Mostly complete list.

    View Slide

  16. View Slide

  17. View Slide

  18. Getting Started
    1. Enable Chrome Dev tools
    // Enable Chrome Dev Tools with chrome://inspect
    WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)

    View Slide

  19. Getting Started
    1.2 Chrome Dev Tools (chrome://inspect)

    View Slide

  20. Getting Started
    1.3 Chrome Dev Tools

    View Slide

  21. Getting Started
    2. Add compat library
    dependencies {
    ...
    implementation 'androidx.webkit:webkit:x.y.z'
    ...
    }

    View Slide

  22. Getting Started
    3. Configure your WebView
    val webView = findViewById(R.id.webView)
    webView.settings.apply {
    ...
    }

    View Slide

  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

    View Slide

  24. Getting Started
    3.2 Caching
    // Overrides the way the cache is used
    cacheMode = WebSettings.LOAD_NO_CACHE

    View Slide

  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

    View Slide

  26. Getting Started
    4.1 Load your remote content3
    val webView = findViewById(R.id.webView)
    val url = "https://api.catz.com/news/content/12345"
    webView.loadUrl(url)
    3 Don't do this - see later slides.

    View Slide

  27. Getting Started
    4.2 Load generated content
    val html = """

    Cats!

    Cats are awesome!

    """
    val htmlBytes = html.toByteArray(charset("UTF-8"))
    val encodedHtml = Base64.encodeToString(htmlBytes, Base64.NO_WRAP)
    webView.loadData(encodedHtml, "text/html", "base64")

    View Slide

  28. Getting Started
    4.3 Loading local content (the right way!)
    val url = "https://catz.androidplatform.net/index.html"
    val webView = findViewById(R.id.webView)
    webView.loadUrl(url)

    View Slide

  29. WebViewClient

    View Slide

  30. WebViewClient

    View Slide

  31. WebViewClient

    View Slide

  32. WebViewClient usage
    — Intercept WebView requests
    — Override network response with custom content
    — Always use WebViewClientCompat!

    View Slide

  33. shouldOverrideUrlLoading()
    — Only for GET requests
    — Don't call WebView.loadUrl() or
    WebView.loadData() here
    — Useful for capturing auth tokens

    View Slide

  34. Capturing OAuth 2 auth code
    val webView = findViewById(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)

    View Slide

  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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  39. Bridging Android and JavaScript

    View Slide

  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
    }
    }

    View Slide

  41. Bridging Android and JavaScript (old version)
    val webView = findViewById(R.id.webView)
    val jsInterface = MyJavaScriptInterface(jsHandler)
    webView.addJavascriptInterface(jsInterface, "android")

    View Slide

  42. Bridging Android and JavaScript (old version)
    function emitEventToAndroid(data) {
    if (android && typeof android.onEvent === 'function') {
    android.onEvent(JSON.stringify(data));
    }
    }

    View Slide

  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!"
    }

    View Slide

  44. Bridging Android and JavaScript (old version)
    — Threading
    — Binding native objects
    — Calling function by strings

    View Slide

  45. Better solution:
    MessageChannel!

    View Slide

  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("*"))

    View Slide

  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));
    }

    View Slide

  48. Customizing local web
    content

    View Slide

  49. ePub standard

    View Slide

  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

    View Slide

  51. ePub structure

    View Slide

  52. ePub structure

    View Slide

  53. Package document



    The Dark World
    ...


    media-type="application/xhtml+xml"/>
    href="images/cover.png"
    media-type="image/png"/>
    href="css/page.css"
    media-type="text/css"/>
    ...






    ...

    ...

    View Slide

  54. ePub scrolling

    View Slide

  55. Wrapping web content (HTML)

    View Slide

  56. Wrapping web content (HTML)


    ePub Reader









    View Slide

  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;
    });
    }

    View Slide

  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;
    }

    View Slide

  59. Demo!

    View Slide

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

    View Slide

  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.

    View Slide

  62. Thank you for listening!
    @ErikHellman
    speakerdeck.com/erikhellman/android-and-the-webview

    View Slide