Unit 6 - Notes

CSE227 13 min read

Unit 6: Web-Based Content

1. Building and Managing Web Apps in WebView

WebView is an Android View that displays web pages. It's a powerful component for creating "hybrid" apps that combine native Android UI with web content. It uses the WebKit rendering engine to display web pages and can include features like forward/backward navigation, zooming, and more.

1.1. Basic WebView Setup

Step 1: Add Internet Permission
Your app must have internet access to load remote web pages. Add this permission to your AndroidManifest.xml:

XML
<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    <application ...>
        ...
    </application>
</manifest>

Step 2: Add WebView to Layout
Include the WebView widget in your XML layout file.

XML
<!-- res/layout/activity_main.xml -->
<WebView
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Step 3: Load a URL
In your Activity or Fragment, get a reference to the WebView and load a URL.

KOTLIN
// In your Activity's onCreate() method
val myWebView: WebView = findViewById(R.id.webview)
myWebView.loadUrl("https://www.android.com")

1.2. Core Components: WebSettings, WebViewClient, and WebChromeClient

Simply loading a URL is not enough for a rich user experience. You need to configure the WebView's behavior.

1.2.1. WebSettings - Configuring WebView Properties

The WebSettings class allows you to manage a wide range of settings for a WebView.

Enabling JavaScript:
By default, JavaScript is disabled in WebView. Most modern websites require it.

KOTLIN
val myWebView: WebView = findViewById(R.id.webview)
myWebView.settings.javaScriptEnabled = true // Crucial for modern web pages

Security Note: Enabling JavaScript can introduce security vulnerabilities (e.g., Cross-Site Scripting). Only load content from trusted sources.

Other Common Settings:

KOTLIN
myWebView.settings.apply {
    // Enable support for the DOM storage API
    domStorageEnabled = true
    
    // Improve rendering performance
    setRenderPriority(WebSettings.RenderPriority.HIGH)
    
    // Enable database storage API
    databaseEnabled = true
    
    // Set the cache mode
    // LOAD_DEFAULT: Default behavior.
    // LOAD_CACHE_ELSE_NETWORK: Use cache if available, else load from network.
    // LOAD_NO_CACHE: Do not use the cache.
    // LOAD_CACHE_ONLY: Only use the cache.
    cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
    
    // Allow file access within WebView
    allowFileAccess = true
}

1.2.2. WebViewClient - Handling Page Navigation and Events

A WebViewClient is essential for controlling how content is loaded. Without it, any link clicked inside your WebView will open in the device's default web browser, taking the user out of your app.

KOTLIN
myWebView.webViewClient = object : WebViewClient() {
    // This is the key method to override.
    // It gives your app control over the URLs the WebView loads.
    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        val url = request?.url.toString()
        // Example: Only load URLs from a specific domain inside the WebView
        return if (Uri.parse(url).host == "www.example.com") {
            // Let the WebView handle the URL
            false
        } else {
            // Otherwise, launch an intent to handle the URL (e.g., in the default browser)
            Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
                startActivity(this)
            }
            // Tell the WebView we've handled the URL
            true
        }
    }

    // Called when the page starts loading
    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        // Show a progress bar
    }

    // Called when the page finishes loading
    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        // Hide the progress bar
    }

    // Handle SSL errors (use with extreme caution)
    override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
        // !! WARNING !!
        // The following line bypasses all SSL certificate validation.
        // It is INSECURE and should NOT be used in production.
        // handler?.proceed() 
        
        // In production, you should show an error to the user and not proceed.
        handler?.cancel()
    }
}

1.2.3. WebChromeClient - Handling Browser UI Events

A WebChromeClient handles browser-specific UI events, such as progress updates, JavaScript dialogs (alert(), confirm(), prompt()), and receiving the page title or favicon.

KOTLIN
myWebView.webChromeClient = object : WebChromeClient() {
    // Reports the loading progress (0-100)
    override fun onProgressChanged(view: WebView?, newProgress: Int) {
        // Update a ProgressBar UI element
        progressBar.progress = newProgress
        if (newProgress == 100) {
            progressBar.visibility = View.GONE
        } else {
            progressBar.visibility = View.VISIBLE
        }
    }

    // Receives the title of the current page
    override fun onReceivedTitle(view: WebView?, title: String?) {
        super.onReceivedTitle(view, title)
        // Update the Activity's title bar
        supportActionBar?.title = title
    }

    // Handle JavaScript alerts
    override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
        AlertDialog.Builder(this@MainActivity)
            .setTitle("Alert")
            .setMessage(message)
            .setPositiveButton("OK") { _, _ -> result?.confirm() }
            .setCancelable(false)
            .show()
        // Return true to indicate we handled the dialog
        return true
    }
}

1.3. JavaScript and Native Code Communication

A key feature of hybrid apps is the ability for web content (JavaScript) and native code (Kotlin/Java) to communicate.

1.3.1. Calling Native Code from JavaScript (addJavascriptInterface)

You can expose methods from a native Kotlin/Java class to your WebView's JavaScript context.

Step 1: Create a JavaScript Interface Class
Create a class whose methods you want to call from JavaScript. Each method must be annotated with @JavascriptInterface.

KOTLIN
class WebAppInterface(private val mContext: Context) {
    /** Show a toast from the web page  */
    @JavascriptInterface
    fun showToast(toast: String) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show()
    }
    
    /** Get Android version and pass to a JS callback */
    @JavascriptInterface
    fun getAndroidVersion(callback: String) {
        val version = Build.VERSION.RELEASE
        // To pass data back, we must call a JS function.
        // This requires running on the UI thread.
        val webView = (mContext as Activity).findViewById<WebView>(R.id.webview)
        webView.post {
            webView.evaluateJavascript("$callback('$version')", null)
        }
    }
}

Step 2: Bind the Interface to WebView
Add an instance of your interface class to the WebView, giving it a name that JavaScript will use to access it (e.g., "Android").

KOTLIN
// In your Activity's onCreate()
myWebView.settings.javaScriptEnabled = true // Must be enabled
myWebView.addJavascriptInterface(WebAppInterface(this), "Android")

Step 3: Call the Native Method from JavaScript
In your web page's JavaScript, you can now call the methods on the object name you defined.

HTML
<!-- in your HTML file -->
<script type="text/javascript">
    function showAndroidToast() {
        // Calls the showToast method in our WebAppInterface class
        Android.showToast("Hello from JavaScript!");
    }

    function onVersionReceived(version) {
        document.getElementById('version').innerText = 'Android Version: ' + version;
    }

    function fetchAndroidVersion() {
        // Calls the getAndroidVersion method, passing a JS callback function name
        Android.getAndroidVersion('onVersionReceived');
    }
</script>

<button onclick="showAndroidToast()">Show Toast</button>
<button onclick="fetchAndroidVersion()">Get Android Version</button>
<p id="version"></p>

1.3.2. Calling JavaScript from Native Code (evaluateJavascript)

This is the modern, asynchronous, and preferred way to execute JavaScript from your native code. It also allows you to receive a return value.

KOTLIN
// Example: Change the background color of the HTML body
val script = "document.body.style.backgroundColor = 'blue';"
myWebView.evaluateJavascript(script, null)

// Example: Call a JS function and get a result
val getTitleScript = "(function() { return document.title; })();"
myWebView.evaluateJavascript(getTitleScript) { value ->
    // value is the result of the JS execution, JSON-encoded.
    // It will be a string like "\"Android WebView Study Notes\""
    val pageTitle = value.replace("\"", "") // Simple un-quoting
    Log.d("WebViewTag", "Page title is: $pageTitle")
}

1.4. Handling Back Button Navigation

By default, pressing the back button will close the Activity, even if the user has navigated through several pages within the WebView. To provide a more natural browser-like experience, you should override onBackPressed() to navigate back in the WebView's history.

KOTLIN
// In your Activity
override fun onBackPressed() {
    val myWebView: WebView = findViewById(R.id.webview)
    if (myWebView.canGoBack()) {
        // If there's history, go back
        myWebView.goBack()
    } else {
        // Otherwise, perform the default back action (exit activity)
        super.onBackPressed()
    }
}

2. Migrating to and Debugging WebView

This section covers best practices for integrating a web application into an Android WebView and how to debug it effectively.

2.1. When to Use WebView vs. Alternatives

Feature WebView Chrome Custom Tabs Browser Intent
UI Control Full control. Embedded in your app's layout. Some control (toolbar color, action button). No control. Leaves your app entirely.
Context User stays within your app. User feels they are in your app, but with browser features. User leaves your app for the browser.
Authentication Shares no cookies with the user's browser. Shares cookies and saved passwords with Chrome. Shares cookies with the default browser.
Communication Full two-way communication via JavaScript Interface. Limited one-way communication to pre-warm the browser. None.
Use Case Hybrid apps, displaying chunks of web UI as part of the native layout. Displaying external links from your app without a jarring context switch. Linking to external content where the user should be in a full browser.

2.2. Optimizing Performance and Offline Support

  • Caching: Use WebSettings.setCacheMode() to control how content is cached. LOAD_CACHE_ELSE_NETWORK is a good default for balancing performance and content freshness.
  • Hardware Acceleration: Ensure hardware acceleration is enabled for your application (it is by default for API 14+). This significantly improves rendering performance and allows for smooth animations.
    XML
        <!-- AndroidManifest.xml -->
        <application android:hardwareAccelerated="true" ...>
        
  • Service Workers: For advanced offline capabilities, the best approach is to implement a Service Worker in your web application. Modern WebView versions support Service Workers, allowing your web app to control its own caching logic and provide a rich offline experience.

2.3. Debugging WebViews

You can use the Chrome DevTools on your desktop to inspect and debug the content of your WebView in real-time.

Step 1: Enable Debugging
In your Application or Activity class, enable web contents debugging. This should only be enabled for debug builds.

KOTLIN
// In your Application's onCreate()
if (BuildConfig.DEBUG) {
    WebView.setWebContentsDebuggingEnabled(true)
}

Step 2: Run Your App
Run your debug build on an emulator or a physical device connected via USB.

Step 3: Use Chrome DevTools

  1. Open Google Chrome on your desktop.
  2. Navigate to chrome://inspect.
  3. Under the "Remote Target" section, you should see your device and the WebView instance with its URL.
  4. Click "inspect" to open a DevTools window for your WebView.

You can now:

  • Inspect the DOM and CSS.
  • Debug JavaScript with breakpoints.
  • View the console for logs and errors.
  • Analyze network requests.

3. Supporting Different Screens in Web Apps

A key benefit of web content is its natural adaptability to different screen sizes. This is achieved through standard responsive web design techniques, which work seamlessly inside a WebView.

3.1. The Viewport Meta Tag

This is the most important element for responsive design. It tells the browser (and WebView) how to control the page's dimensions and scaling. Without it, the WebView will assume a desktop-sized screen and scale the content down, making it unreadable.

Include this in the <head> of your HTML:

HTML
<meta name="viewport" content="width=device-width, initial-scale=1.0">

  • width=device-width: Sets the width of the page to follow the screen-width of the device.
  • initial-scale=1.0: Establishes a 1:1 relationship between CSS pixels and device-independent pixels.

3.2. Responsive Web Design (RWD) Techniques

Your web content should use standard RWD practices. WebView will render them correctly.

  • CSS Media Queries: Apply different CSS styles based on screen characteristics like width, height, orientation, and resolution.
    CSS
        /* Base styles for mobile */
        .container { width: 100%; }
    
        /* Styles for tablets and larger screens */
        @media (min-width: 768px) {
            .container { width: 80%; margin: 0 auto; }
        }
        
  • Flexible Grids: Use CSS Flexbox or CSS Grid to create layouts that adapt to the available space.
  • Relative Units: Use relative units like %, vw (viewport width), vh (viewport height), em, and rem for sizing, rather than fixed pixel values.

3.3. WebSettings for Screen Support

WebView provides settings that help manage how content is displayed, especially for non-responsive websites.

KOTLIN
myWebView.settings.apply {
    // This setting makes the WebView use a "wide" viewport, similar to a desktop browser.
    // Useful for sites not optimized for mobile.
    useWideViewPort = true

    // When useWideViewPort is true, this zooms the content out to fit the screen width on initial load.
    loadWithOverviewMode = true

    // Enable pinch-to-zoom
    setSupportZoom(true)
    
    // Show the on-screen zoom controls (usually not recommended for a clean UI)
    builtInZoomControls = true
    
    // Hide the on-screen zoom controls when builtInZoomControls is true
    displayZoomControls = false
}

For modern, responsive websites built with the viewport meta tag, useWideViewPort and loadWithOverviewMode are often not necessary and can sometimes interfere with the intended layout.

4. Advanced Android Programming with WebView

This section covers advanced security, performance, and customization topics.

4.1. Security Best Practices

WebView can be a significant security risk if not configured properly.

  • Limit JavaScript Interface Exposure: Only expose the minimal set of methods required via @JavascriptInterface. Never expose sensitive Android APIs.
  • Disable File System Access: Unless you explicitly need it, disable file access to prevent malicious scripts from accessing the local file system.
    KOTLIN
        myWebView.settings.allowFileAccess = false
        
  • Handle Content Carefully: Only load content from trusted, HTTPS-secured sources. Avoid loading content from http:// URLs.
  • Use androidx.webkit Library: The Jetpack WebView library provides modern security features, like SafeBrowsing, on older Android versions.
    GROOVY
        // build.gradle
        implementation "androidx.webkit:webkit:1.6.0"
        
  • Content Security Policy (CSP): Implement a strong CSP via HTTP headers on your web server. This is a critical defense-in-depth measure that instructs the WebView on which sources of content (scripts, styles, images) are trusted.

4.2. Performance and Memory Management

A WebView can consume a large amount of memory and, if mismanaged, can lead to memory leaks.

The Context Memory Leak Problem:
A WebView holds a strong reference to the Context it was initialized with. If this is an Activity context, the WebView will prevent the Activity from being garbage collected after it's destroyed (e.g., on screen rotation), leading to a memory leak.

Solution:

  1. Do not reference WebView from a static field.
  2. Properly destroy the WebView in your Activity/Fragment's onDestroy/onDestroyView method.

    KOTLIN
        // In your layout XML, wrap the WebView in a container
        <FrameLayout
            android:id="@+id/webview_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        

    KOTLIN
        // In your Activity/Fragment class
        private var webView: WebView? = null
        
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            // Create WebView programmatically with application context if possible,
            // but often an Activity context is needed for dialogs.
            // The key is the cleanup in onDestroyView.
            webView = WebView(requireContext()).apply {
                // ... configure settings, clients, etc.
            }
            val container: FrameLayout = view.findViewById(R.id.webview_container)
            container.addView(webView)
            // ... load URL
        }
        
        override fun onDestroyView() {
            // This is the crucial part
            val container: FrameLayout = view.findViewById(R.id.webview_container)
            container.removeAllViews()
            webView?.destroy()
            webView = null
            super.onDestroyView()
        }
        

4.3. Advanced Customization

4.3.1. Intercepting Network Requests

You can intercept any network request made by the WebView using shouldInterceptRequest in your WebViewClient. This allows you to serve content from cache, from local assets, or modify the request.

KOTLIN
myWebView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        val url = request?.url.toString()
        // Example: If the WebView requests a specific JS file,
        // serve it from the app's local assets instead.
        if (url.endsWith("local_script.js")) {
            return try {
                val inputStream = context.assets.open("local_script.js")
                WebResourceResponse("text/javascript", "UTF-8", inputStream)
            } catch (e: IOException) {
                null // Let WebView handle it if the file is not found
            }
        }
        return super.shouldInterceptRequest(view, request)
    }
}

The androidx.webkit.WebViewAssetLoader provides a much simpler and more robust way to achieve this for local assets and resources.

4.3.2. Handling File Uploads

By default, <input type="file"> does not work in a WebView. You must handle the onShowFileChooser callback in WebChromeClient to open a file picker.

KOTLIN
class MyActivity : AppCompatActivity() {
    
    private var filePathCallback: ValueCallback<Array<Uri>>? = null

    private val fileChooserLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val data = result.data
            // data.data is for single file, data.clipData is for multiple
            val uris = data?.data?.let { arrayOf(it) } ?:
                       data?.clipData?.let { clip -> (0 until clip.itemCount).map { clip.getItemAt(it).uri } }?.toTypedArray()
            filePathCallback?.onReceiveValue(uris)
        } else {
            filePathCallback?.onReceiveValue(null)
        }
        filePathCallback = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        // ... setup
        myWebView.webChromeClient = object : WebChromeClient() {
            override fun onShowFileChooser(
                webView: WebView?,
                filePathCallback: ValueCallback<Array<Uri>>?,
                fileChooserParams: FileChooserParams?
            ): Boolean {
                this@MyActivity.filePathCallback = filePathCallback
                val intent = fileChooserParams?.createIntent()
                try {
                    fileChooserLauncher.launch(intent)
                } catch (e: ActivityNotFoundException) {
                    Toast.makeText(this@MyActivity, "Cannot open file chooser", Toast.LENGTH_LONG).show()
                    return false
                }
                return true
            }
        }
    }
}