Unit 6 - Notes
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:
<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.
<!-- 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.
// 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.
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:
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.
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.
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.
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").
// 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.
<!-- 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.
// 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.
// 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_NETWORKis 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
WebViewversions 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.
// 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
- Open Google Chrome on your desktop.
- Navigate to
chrome://inspect. - Under the "Remote Target" section, you should see your device and the
WebViewinstance with its URL. - 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:
<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, andremfor 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.
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.
KOTLINmyWebView.settings.allowFileAccess = false - Handle Content Carefully: Only load content from trusted, HTTPS-secured sources. Avoid loading content from
http://URLs. - Use
androidx.webkitLibrary: The Jetpack WebView library provides modern security features, likeSafeBrowsing, 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
WebViewon 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:
- Do not reference
WebViewfrom a static field. -
Properly destroy the
WebViewin yourActivity/Fragment'sonDestroy/onDestroyViewmethod.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.
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.
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
}
}
}
}