Scientyfic World

How to Convert a Website Into an Android App (Kotlin WebView, 2026)

If you already have a responsive website, you can ship it as an Android app by loading it in a WebView: a full-screen browser view inside your own app. This...

Share:

Get an AI summary of this article

Convert website into Android App article banner with one laptop and an android device opening scientyfic world home page

If you already have a responsive website, you can ship it as an Android app by loading it in a WebView: a full-screen browser view inside your own app. This guide builds that app from scratch in Kotlin with today’s Android APIs, then covers the alternative Google recommends for progressive web apps, the Trusted Web Activity. I built the project below with Android Gradle Plugin 9.4 and ran it on an Android 16 emulator, and I note what I tested as we go.

The original 2022 version of this post used Java, NetworkInfo, onBackPressed() and fill_parent. All of those are deprecated or obsolete now, so the code is rewritten rather than patched.

Before You Start: Is a WebView App Allowed?

Only wrap a website you own or have permission to use. Google Play’s spam policy does not allow apps whose primary purpose is to show a webview of a website without the site owner’s permission, and it expects apps to offer real value rather than repeat content. A thin wrapper around your own site can still be rejected if it adds nothing, so consider what the app does that the mobile site does not, such as notifications, offline support or deep links.

WebView or Trusted Web Activity?

WebView (this tutorial)Trusted Web Activity
What it isA browser view embedded in your appYour site opened full-screen in the user’s browser (usually Chrome), without browser UI
Best forCustom native UI around the site, control over navigation, JavaScript bridgesA progressive web app you own, shown exactly as in the browser
Browser featuresSeparate cookies and storage from the browser; you handle uploads, permissions and errorsShares the browser’s engine, logins and updates
RequirementsAndroid Studio or GradleDigital Asset Links file on your domain; Bubblewrap CLI or Android Studio

Android’s documentation suggests a Trusted Web Activity when you want to deliver your own site as an app with a full browser experience, and a WebView when your app needs to embed and control the web content. I cover the TWA route at the end.

What You Need

  • Android Studio (or the command-line SDK tools and Gradle). Android Studio bundles a suitable JDK.
  • A website that works well on a phone-sized screen and is served over HTTPS.
  • An emulator or an Android phone with USB debugging enabled for testing.

Step 1: Create the Project

In Android Studio choose New Project. The default “Empty Activity” template now creates a Jetpack Compose app; for this tutorial pick Empty Views Activity, or start with any template and replace the files with the ones below. I used Kotlin, minimum SDK 24 (Android 7.0) and package name com.example.websiteapp; change the package and app name to your own.

The app module’s build file. Android Gradle Plugin 9 compiles Kotlin without a separate Kotlin plugin:

plugins {
    id("com.android.application")
}

android {
    namespace = "com.example.websiteapp"
    compileSdk = 37

    defaultConfig {
        applicationId = "com.example.websiteapp"
        minSdk = 24
        targetSdk = 37
        versionCode = 1
        versionName = "1.0"
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
        }
    }
}

dependencies {
    implementation("androidx.activity:activity-ktx:1.13.0")
    implementation("androidx.core:core-ktx:1.19.1")
    implementation("androidx.webkit:webkit:1.17.1")
}

I first compiled against SDK 36, and the build failed because the current AndroidX libraries require compile SDK 37, so the file targets 37. If you use older library versions, lower these numbers to match. Lint also asked for isShrinkResources alongside minification, which shrinks the release APK.

Step 2: Declare Internet Access

The manifest needs only the INTERNET permission. The old tutorial also requested ACCESS_NETWORK_STATE to check connectivity before loading; this version instead reacts to load errors, which is more reliable and needs no extra permission.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

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

    <application
        android:label="@string/app_name"
        android:icon="@android:drawable/sym_def_app_icon"
        android:theme="@android:style/Theme.Material.Light.NoActionBar">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Replace the placeholder android:icon with your own launcher icon: in Android Studio, right-click res, choose New > Image Asset, and pick Launcher Icons (Adaptive and Legacy). The old post chose “Action Bar and Tab Icons”, which creates toolbar icons rather than an app icon.

App strings go in res/values/strings.xml:

<resources>
    <string name="app_name">My Website</string>
    <string name="offline_message">You appear to be offline. Check your connection and try again.</string>
    <string name="retry">Try again</string>
</resources>

Step 3: Write MainActivity

This activity builds its layout in code, so there is no XML layout file to keep in sync. It loads your site, keeps your own pages inside the app and opens other links in the browser, handles Back through the page history, shows a retry screen when the page cannot load, survives rotation, and handles insets and renderer crashes.

package com.example.websiteapp

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.webkit.RenderProcessGoneDetail
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding

class MainActivity : ComponentActivity() {

    // Change this to your own site. Only publish an app for a site you own.
    private val homeUrl = "https://example.com/"
    private val homeHost = "example.com"

    private lateinit var webView: WebView
    private lateinit var offlineView: LinearLayout

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        webView = WebView(this).apply {
            settings.javaScriptEnabled = true   // only because your own site needs it
            settings.domStorageEnabled = true
            webViewClient = SiteClient()
        }

        offlineView = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            gravity = Gravity.CENTER
            isVisible = false
            addView(TextView(context).apply { setText(R.string.offline_message); gravity = Gravity.CENTER })
            addView(Button(context).apply {
                setText(R.string.retry)
                setOnClickListener { showOffline(false); webView.reload() }
            })
        }

        val root = FrameLayout(this).apply {
            addView(webView)
            addView(offlineView)
        }
        setContentView(root)

        // Keep content clear of the status bar, navigation bar and display cutouts.
        ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
            val bars = insets.getInsets(
                WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
            )
            view.updatePadding(bars.left, bars.top, bars.right, bars.bottom)
            WindowInsetsCompat.CONSUMED
        }

        // Back goes through the site's history first, then exits.
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (webView.canGoBack()) {
                    webView.goBack()
                } else {
                    isEnabled = false
                    onBackPressedDispatcher.onBackPressed()
                }
            }
        })

        if (savedInstanceState != null) {
            webView.restoreState(savedInstanceState)
        } else {
            webView.loadUrl(homeUrl)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        webView.saveState(outState)
    }

    override fun onDestroy() {
        webView.destroy()
        super.onDestroy()
    }

    private fun showOffline(show: Boolean) {
        offlineView.isVisible = show
        webView.isVisible = !show
    }

    private inner class SiteClient : WebViewClient() {
        // Your own pages stay in the app; everything else opens in the browser.
        override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
            val host = request.url.host ?: return false
            if (host == homeHost || host.endsWith(".$homeHost")) return false
            try {
                startActivity(Intent(Intent.ACTION_VIEW, request.url))
            } catch (e: ActivityNotFoundException) {
                // No app can handle this link; ignore it.
            }
            return true
        }

        // Show a retry screen when the main page fails to load, for example offline.
        override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
            if (request.isForMainFrame) showOffline(true)
        }

        // If the WebView's renderer crashes or is killed for memory, rebuild the
        // screen instead of letting the whole app crash.
        override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
            recreate()
            return true
        }
    }
}

What each part does, and why it changed

  • Edge-to-edge and insets. Apps targeting SDK 35 or higher are drawn edge-to-edge on Android 15 and later, so without handling insets your site would sit under the status bar. The inset listener adds padding for the system bars and display cutout.
  • Links. shouldOverrideUrlLoading now receives a WebResourceRequest. Returning false lets the WebView load your own pages; other hosts open in the user’s browser. The old version called view.loadUrl(url) inside this method, which the WebView documentation advises against.
  • Back navigation. onBackPressed() is deprecated. An OnBackPressedCallback goes back through the site’s history and, when there is none, lets the system close the app, which also works with Android’s predictive back gesture. I dropped the old “Are you sure you want to exit?” dialog; Android apps normally just close.
  • Offline handling. Instead of the deprecated NetworkInfo check, the app shows a retry screen when the main page fails to load.
  • Renderer crashes. If the WebView’s renderer process dies, for example when the system reclaims memory, an app without onRenderProcessGone crashes. Android lint flagged this in my first build, so the activity now recreates itself instead.
  • JavaScript. It is off by default in WebView. Enable it only because your own site needs it, and never use addJavascriptInterface with content you do not control.

Step 4: Build and Test

Run the app from Android Studio, or build from the command line with ./gradlew assembleDebug and install the APK with adb install. On my machine the debug APK was about 2.9 MB, and the minified release APK about 117 KB. This is what I checked on an Android 16 emulator, with example.com as the home URL:

TestResult
LaunchSite loaded full-screen, with content below the status bar
Tap a link to another domainOpened in Chrome, not inside the app
Press Back from ChromeReturned to the app on the same page
Launch offline after a previous visitWebView showed the cached copy of the page
Launch offline with no cached dataRetry screen appeared
Reconnect and tap Try againSite loaded
Android lint0 errors; one remaining warning on the WebViewClient constructor about onRenderProcessGone, which the class already implements

Test on a real phone too, and walk through your site’s own flows: logins, forms, file uploads and payments often need extra work in a WebView. File uploads, for example, require a WebChromeClient with onShowFileChooser, and sites that use Google sign-in may block it inside embedded WebViews.

Step 5: Create a Release Build for Google Play

Google Play requires an Android App Bundle (AAB) for new apps. In Android Studio, use Build > Generate Signed App Bundle or APK, create or choose an upload key, and build the release variant. The video below, recorded with the 2022 version of Android Studio, shows the signing flow; menu names may differ slightly in current versions.

Keep the upload key and its password safe and backed up. Before publishing, check Play’s current target SDK requirement and complete the store listing, data safety form and content rating.

Alternative: A Trusted Web Activity With Bubblewrap

If your site is a progressive web app with a web app manifest, a Trusted Web Activity gives users the full browser experience, including saved logins and the browser’s security updates, with almost no Android code. Google’s Bubblewrap CLI generates the project from your manifest:

npm i -g @bubblewrap/cli
bubblewrap init --manifest=https://your-site.com/manifest.json
bubblewrap build

You then publish a Digital Asset Links file at https://your-site.com/.well-known/assetlinks.json containing your app’s package name and signing certificate fingerprint. If you use Play App Signing, include the fingerprint of the key Google signs with, not just your upload key. Without a valid asset link, the app falls back to a Custom Tab with a visible browser address bar. I did not build a TWA for this post.

Conclusion

Turning a website into an Android app takes one Kotlin activity: a WebView, sensible link handling, Back support, an offline screen, inset handling and a renderer-crash fallback. Use it for a site you own, test the flows that matter on a real device, and give the app a reason to exist beyond the mobile site. If your site is already a PWA, a Trusted Web Activity is often the simpler path. For apps that need more native screens, look at React Native instead.

FAQs

Can I convert any website into an Android app?

Technically yes, but only publish an app for a website you own or have permission to use. Google Play does not allow apps whose main purpose is to show someone else’s website in a webview without permission, and thin wrappers that add no value can be rejected.

Should I use a WebView or a Trusted Web Activity?

Use a Trusted Web Activity when your own site is a progressive web app and you want the full browser experience with shared logins. Use a WebView when you need native UI around the site, control over navigation, or a bridge between JavaScript and native code.

Why does my site appear under the status bar?

Apps that target SDK 35 or higher are displayed edge-to-edge on Android 15 and later. Handle window insets, for example with ViewCompat.setOnApplyWindowInsetsListener, to add padding for the system bars.

How do I make the Back button go to the previous page?

Register an OnBackPressedCallback that calls webView.goBack() when webView.canGoBack() is true, and otherwise disables itself and passes Back to the system. The old onBackPressed() override is deprecated.

Does a WebView app work offline?

Only partly. The WebView can show cached copies of pages it has loaded before, but a fresh page needs a connection. Show a retry screen when loading fails, or add a service worker to your site for real offline support.

Do I need Java or Kotlin?

Either works, but Kotlin is Google’s recommended language for Android, and current documentation and samples are written in Kotlin.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts