Skip to content
Tolinku
Tolinku
Sign In Start Free
Android Development · · 6 min read

Android WebView and App Links: Handling Deep Links

By Tolinku Staff
|
Tolinku app links dashboard screenshot for android blog posts

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

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

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

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.