WebView is Android's embedded browser component. It renders web content inside your app, which is useful for displaying help pages, terms of service, web-based features, and hybrid app content. But when that web content contains deep links (App Links, custom schemes, or intent URLs), WebView's default behavior is almost always wrong.
By default, WebView loads everything internally. A tap on an App Link loads the target page inside the WebView instead of opening the target app. A tap on a tel: link shows an error instead of opening the dialer. A tap on a mailto: link does nothing useful. Every link type that should leave the WebView stays trapped inside it.
This guide covers how to configure WebView to handle deep links correctly. For Chrome Custom Tabs (the alternative to WebView for in-app browsing), see the Custom Tabs guide. For the App Links setup, see the Android App Links complete guide.
The Core Problem
When a user taps a link inside a WebView, the WebView's WebViewClient decides what to do. The default behavior:
// Default WebView behavior (simplified)
fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
return false // Load everything inside the WebView
}
Returning false means "handle this URL internally." The WebView loads the page, even if that URL is an App Link that should open another app, a phone number, or a deep link back into your own native navigation.
Configuring shouldOverrideUrlLoading
Basic Configuration
Override shouldOverrideUrlLoading to intercept navigation and route URLs appropriately:
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val url = request.url
val scheme = url.scheme
return when {
// Handle custom schemes (tel:, mailto:, etc.)
scheme == "tel" || scheme == "mailto" || scheme == "sms" -> {
val intent = Intent(Intent.ACTION_VIEW, url)
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
// No handler for this scheme
}
true // We handled it; don't load in WebView
}
// Handle intent:// URLs
scheme == "intent" -> {
handleIntentUrl(url.toString())
true
}
// Handle deep links to your own app
isOwnAppLink(url) -> {
handleOwnDeepLink(url)
true
}
// Handle external App Links (open in browser/target app)
isExternalLink(url) -> {
val intent = Intent(Intent.ACTION_VIEW, url)
startActivity(intent)
true
}
// Load everything else in the WebView
else -> false
}
}
}
Handling Your Own Deep Links
When web content inside your WebView contains links back to your own app (e.g., a link from your help center to a product page), you should handle those natively rather than loading them in the WebView:
fun isOwnAppLink(url: Uri): Boolean {
val ownHosts = setOf("yourapp.com", "links.yourapp.com", "go.yourapp.com")
return url.scheme == "https" && url.host in ownHosts
}
fun handleOwnDeepLink(url: Uri) {
// Route to native navigation
when {
url.path?.startsWith("/products") == true -> {
val productId = url.lastPathSegment
startActivity(ProductActivity.intent(this, productId))
}
url.path?.startsWith("/settings") == true -> {
startActivity(SettingsActivity.intent(this))
}
else -> {
// Unknown path; open externally
startActivity(Intent(Intent.ACTION_VIEW, url))
}
}
}
Handling Intent URLs
Intent URLs (intent://...) are a common mechanism for web-to-app deep linking on Android. WebView doesn't handle them by default:
fun handleIntentUrl(url: String) {
try {
val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
// Security: verify the intent targets a browsable activity
intent.addCategory(Intent.CATEGORY_BROWSABLE)
intent.component = null
intent.selector = null
startActivity(intent)
} catch (e: Exception) {
// Parse failed or no handler; try the fallback URL
val fallbackUrl = Uri.parse(url).getQueryParameter("S.browser_fallback_url")
if (fallbackUrl != null) {
webView.loadUrl(fallbackUrl)
}
}
}
Handling External Links
For links that point to other domains (not your own), decide whether to open them in the WebView, in Chrome Custom Tabs, or via the system browser:
fun isExternalLink(url: Uri): Boolean {
val ownHosts = setOf("yourapp.com", "links.yourapp.com", "help.yourapp.com")
return url.scheme == "https" && url.host !in ownHosts
}
For external links, opening them in the default browser (via Intent.ACTION_VIEW) is usually the right choice. This lets App Links work correctly: if the link targets another app, that app will open. If it's a regular web URL, Chrome (or the default browser) handles it.
Common Pitfalls
Pitfall 1: Infinite Loop Between WebView and App
If your WebView loads a page from your own domain, and that page contains links to your own domain, and shouldOverrideUrlLoading catches those links and routes them to your native navigation, you might end up in a loop:
WebView loads help.yourapp.com
→ User taps link to yourapp.com/products/123
→ shouldOverrideUrlLoading catches it
→ Opens ProductActivity
→ ProductActivity has a WebView showing yourapp.com/products/123
→ That page has links to yourapp.com/...
→ Loop continues
Fix: Distinguish between "links that should navigate natively" and "links that should load in the current WebView." Use path-based rules:
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
// Links within the help center: stay in WebView
if (url.host == "help.yourapp.com") return false
// Deep links to product pages: navigate natively
if (url.host == "yourapp.com" && url.path?.startsWith("/products") == true) {
handleOwnDeepLink(url)
return true
}
// Everything else: stay in WebView
return false
}
Pitfall 2: JavaScript-Triggered Navigation
shouldOverrideUrlLoading is NOT called for all navigation events. Notably:
window.locationchanges from JavaScript may not trigger it consistently.- Form submissions may not trigger it.
- Redirects (302) may not trigger it on all Android versions.
For JavaScript-initiated navigation, also override onPageStarted:
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
val uri = Uri.parse(url)
// Catch redirects to external domains
if (isExternalLink(uri)) {
view.stopLoading()
startActivity(Intent(Intent.ACTION_VIEW, uri))
return
}
super.onPageStarted(view, url, favicon)
}
Pitfall 3: Missing WebViewClient
If you don't set a WebViewClient at all, the WebView opens ALL links in the system browser. This is the opposite problem: nothing loads in the WebView.
// Without this, every link opens externally
webView.webViewClient = WebViewClient() // At minimum, set the default client
Pitfall 4: POST Requests
shouldOverrideUrlLoading only fires for GET requests (user-initiated navigation). POST requests (form submissions) are handled internally by the WebView. If a form submission redirects to a deep link URL, you need to catch it in onPageStarted or doUpdateVisitedHistory.
Pitfall 5: SSL Errors in WebView
If your deep link landing page has SSL issues (expired certificate, mismatched domain), the WebView shows a blank page with no error indication. This is different from Chrome, which shows a full-page warning.
Always handle SSL errors:
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
// Don't call handler.proceed() in production; that bypasses SSL verification
handler.cancel()
showSslErrorMessage()
}
WebView vs. Chrome Custom Tabs
For most "open a link inside the app" use cases, Chrome Custom Tabs are better than WebView:
| Feature | WebView | Chrome Custom Tabs |
|---|---|---|
| App Link support | Manual (via shouldOverrideUrlLoading) | Automatic |
| Password autofill | No | Yes (Chrome's password manager) |
| Cookie sharing with Chrome | No | Yes |
| Performance | Slower (separate rendering engine instance) | Faster (shares Chrome's process) |
| Security updates | Depends on system WebView version | Updated with Chrome |
| Deep link handling | Manual configuration required | Mostly automatic |
Use WebView when: You need full control over the rendered content (hybrid app features, custom JavaScript bridges, inline content).
Use Custom Tabs when: You're opening external or semi-external web content (help pages, OAuth flows, partner pages).
Integration with Tolinku
When your WebView loads pages that contain Tolinku deep links, ensure shouldOverrideUrlLoading handles the Tolinku redirect chain correctly:
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
val host = url.host ?: ""
// Let Tolinku links open externally (they redirect to the target app)
if (host.endsWith("tolinku.com") || host.endsWith("tolk.link")) {
startActivity(Intent(Intent.ACTION_VIEW, url))
return true
}
// ... rest of your routing logic
return false
}
This ensures Tolinku's platform routing (app detection, deferred deep linking, fallback handling) works correctly rather than being trapped inside the WebView.
For the full App Links setup, see the Android App Links guide. For the Tolinku SDK integration, the SDK handles deep link routing automatically in native Activities.
Get deep linking tips in your inbox
One email per week. No spam.