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 is | A browser view embedded in your app | Your site opened full-screen in the user’s browser (usually Chrome), without browser UI |
| Best for | Custom native UI around the site, control over navigation, JavaScript bridges | A progressive web app you own, shown exactly as in the browser |
| Browser features | Separate cookies and storage from the browser; you handle uploads, permissions and errors | Shares the browser’s engine, logins and updates |
| Requirements | Android Studio or Gradle | Digital 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.
shouldOverrideUrlLoadingnow receives aWebResourceRequest. Returningfalselets the WebView load your own pages; other hosts open in the user’s browser. The old version calledview.loadUrl(url)inside this method, which the WebView documentation advises against. - Back navigation.
onBackPressed()is deprecated. AnOnBackPressedCallbackgoes 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
NetworkInfocheck, 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
onRenderProcessGonecrashes. 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
addJavascriptInterfacewith 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:
| Test | Result |
|---|---|
| Launch | Site loaded full-screen, with content below the status bar |
| Tap a link to another domain | Opened in Chrome, not inside the app |
| Press Back from Chrome | Returned to the app on the same page |
| Launch offline after a previous visit | WebView showed the cached copy of the page |
| Launch offline with no cached data | Retry screen appeared |
| Reconnect and tap Try again | Site loaded |
| Android lint | 0 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 buildYou 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.








