Android Apps

How to Convert a Website into an Android App

Wrap your website in an Android app with WebView. Full code, offline handling, back button, file uploads — and the Play Store rule that gets plain wrappers rejected.

A website running inside an Android WebView app

You can put your website inside an Android app in about twenty minutes. Android has a component called WebView that displays web pages, and wrapping your site in one is a genuinely reasonable thing to do.

But read the next section first, because it decides whether your app ever reaches the Play Store.

The rule nobody mentions

Google Play policy rejects apps whose only function is to display a website. The wording is about apps that provide "no independent value" — a browser with your logo on it.

This gets apps rejected every day. If you plan to publish, the app must do at least one thing the website cannot:

  • Push notifications — the strongest and most common answer
  • Offline access to some content
  • Camera or file uploads handled natively
  • A home screen widget
  • Biometric login
  • Native navigation — a real bottom bar rather than the website menu

If the app is only for internal use, a company intranet, or side-loading to your own staff, none of this applies. Build the wrapper and move on.

When a WebView app is the right choice

Good fitPoor fit
Your site is already mobile-friendlyThe site is not responsive
Content changes often and you do not want app updatesYou need smooth animation or games
Internal tools and dashboardsHeavy camera or sensor use
A small budget and one codebaseYou want the app to feel truly native

Be honest about the first row. If your website looks bad on a phone, it will look exactly as bad inside the app. Fix the website first — that work benefits everyone, not just app users.

Step 1: Create the project

File → New → New Project → Empty Views Activity. Package name com.yourname.yourapp, minimum SDK API 24.

Step 2: Permissions

In AndroidManifest.xml, above <application>:

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

Without the internet permission the WebView shows a blank white screen and no error, which wastes a surprising amount of debugging time.

Step 3: The layout

res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipeRefresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

Wrapping it in SwipeRefreshLayout gives you pull-to-refresh, which people expect. Add the dependency in build.gradle:

implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

Step 4: The code

package com.yourname.yourapp;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

public class MainActivity extends AppCompatActivity {

    private WebView webView;
    private SwipeRefreshLayout swipeRefresh;

    private static final String HOME_URL = "https://yourwebsite.com";

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView      = findViewById(R.id.webView);
        swipeRefresh = findViewById(R.id.swipeRefresh);

        WebSettings s = webView.getSettings();
        s.setJavaScriptEnabled(true);          // nearly every site needs this
        s.setDomStorageEnabled(true);          // localStorage — logins break without it
        s.setLoadWithOverviewMode(true);
        s.setUseWideViewPort(true);
        s.setSupportZoom(false);               // a proper app does not pinch-zoom
        s.setCacheMode(WebSettings.LOAD_DEFAULT);

        webView.setWebViewClient(new WebViewClient() {

            // Keep links inside the app, but send other domains to the browser
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                String url = request.getUrl().toString();
                if (url.contains("yourwebsite.com")) {
                    return false;              // load it here
                }
                startActivity(new android.content.Intent(
                        android.content.Intent.ACTION_VIEW, request.getUrl()));
                return true;                   // hand it to the browser
            }

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                swipeRefresh.setRefreshing(true);
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                swipeRefresh.setRefreshing(false);
            }

            @Override
            public void onReceivedError(WebView view, WebResourceRequest req, WebResourceError err) {
                if (req.isForMainFrame()) {
                    view.loadUrl("file:///android_asset/offline.html");
                }
            }
        });

        swipeRefresh.setOnRefreshListener(() -> webView.reload());

        // Back button should walk back through pages, not close the app
        getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {
                if (webView.canGoBack()) {
                    webView.goBack();
                } else {
                    setEnabled(false);
                    getOnBackPressedDispatcher().onBackPressed();
                }
            }
        });

        webView.loadUrl(HOME_URL);
    }
}

Step 5: The offline page

Without this, losing signal shows the ugly grey "Webpage not available" screen. Create app/src/main/assets/offline.html:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { font-family: system-ui, sans-serif; display: flex; flex-direction: column;
           align-items: center; justify-content: center; height: 100vh; margin: 0;
           background: #f8fafc; color: #334155; text-align: center; padding: 24px; }
    h1 { font-size: 1.4rem; margin-bottom: 8px; color: #0f172a; }
    button { margin-top: 20px; padding: 12px 26px; border: none; border-radius: 999px;
             background: #0d9488; color: #fff; font-size: 1rem; }
  </style>
</head>
<body>
  <h1>No internet connection</h1>
  <p>Check your connection and try again.</p>
  <button onclick="location.reload()">Retry</button>
</body>
</html>

Step 6: File uploads

If your site has any file or photo upload, it silently does nothing in a WebView until you add this. Users tap the button, no picker opens, and they assume the app is broken.

import android.content.Intent;
import android.net.Uri;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;

private ValueCallback<Uri[]> filePathCallback;

private final ActivityResultLauncher<Intent> filePicker =
    registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
        if (filePathCallback == null) return;
        Uri[] results = null;
        if (result.getResultCode() == RESULT_OK && result.getData() != null) {
            Uri uri = result.getData().getData();
            if (uri != null) { results = new Uri[]{uri}; }
        }
        filePathCallback.onReceiveValue(results);
        filePathCallback = null;
    });

// add inside onCreate, after the WebViewClient:
webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> callback,
                                     FileChooserParams params) {
        filePathCallback = callback;
        filePicker.launch(params.createIntent());
        return true;
    }
});

Step 7: Make it feel like an app

Four small things that change the impression completely:

  1. A splash screen, so launch does not begin with a white flash
  2. A proper app icon — File → New → Image Asset
  3. Theme colours that match your site, so the status bar is not a mismatched grey
  4. Hide the website header inside the app. Inject a small CSS rule in onPageFinished to remove the site navigation, since the app has its own.

Common problems

A blank white screen

Missing internet permission, or an http:// URL. Since Android 9 plain HTTP is blocked by default — use HTTPS. See how to install a free SSL certificate.

The site loads but looks broken

JavaScript is off. Set setJavaScriptEnabled(true), and setDomStorageEnabled(true) as well — many sites store their session in localStorage and log the user straight back out without it.

Links open in Chrome instead of the app

You are missing shouldOverrideUrlLoading, or your domain check does not match. Watch for www. versus the bare domain.

The back button closes the app

Add the OnBackPressedCallback above. The old onBackPressed() override is deprecated.

File upload does nothing

onShowFileChooser is missing. Nothing appears and nothing errors, which is why this one is so confusing.

Play Store rejected the app

Policy 4.4 — no independent value. Add push notifications or offline content and resubmit with a note explaining what the app does beyond the website.

Is there a better option?

Two worth knowing about:

A Progressive Web App (PWA) gives users an installable icon with offline support and push notifications, with no app store at all. Cheaper and faster if store presence is not the goal.

Trusted Web Activity (TWA) is Google's own way of shipping a website as an app. It uses Chrome under the hood, so the site looks identical and there is no address bar. It also passes review more easily than a WebView wrapper, because Google designed it for this.

If you want an app icon on the Play Store for an existing site, TWA is usually the better answer than WebView. Use WebView when you need to mix web content with native features.

Where to go next

Questions people ask

Will Google Play accept an app that just shows my website?
Usually not. Play policy rejects apps with no independent value beyond a website. Add at least one native feature — push notifications, offline content, file uploads or a widget — before submitting. Internal or side-loaded apps are not affected.
Why is my WebView showing a blank white screen?
Either the internet permission is missing from the manifest, or the URL uses http rather than https. Android has blocked plain HTTP traffic by default since Android 9, and it fails silently with no error message.
Why does my site look broken inside the app?
JavaScript is disabled by default in a WebView. Set setJavaScriptEnabled(true) and also setDomStorageEnabled(true), because many sites keep their login session in localStorage and will log users out without it.
How do I make the back button go to the previous page?
Add an OnBackPressedCallback that calls webView.goBack() while canGoBack() is true, and only closes the app once there is no history left. The older onBackPressed override is deprecated.
Why does file upload not work in my WebView app?
You need to implement onShowFileChooser in a WebChromeClient. Without it, tapping an upload button does nothing at all — no picker, no error — so users assume the app is broken.
Should I use WebView or a Trusted Web Activity?
Use a Trusted Web Activity if you simply want your existing site as an app. It runs on Chrome, looks identical to the real site and passes Play review more easily because Google designed it for that. Use WebView when you need to mix web content with native features.
Will my website work well as an app?
Only if it is already mobile-friendly. A WebView displays exactly what the browser would, so a site that looks bad on a phone looks equally bad in the app. Fix the responsive design first — that helps everyone, not just app users.
How do I stop external links opening inside my app?
Implement shouldOverrideUrlLoading, return false for URLs on your own domain and launch an Intent for everything else. Remember to handle both the www and non-www forms of your domain in the check.
How do I show something useful when there is no internet?
Put an offline.html file in the assets folder and load it from onReceivedError when the failure is for the main frame. Otherwise users see the grey system error page, which looks like the app has crashed.
Is a PWA better than a WebView app?
For many projects, yes. A Progressive Web App gives an installable icon, offline support and push notifications with no app store involved, and no separate codebase. Choose a WebView or TWA only when Play Store presence genuinely matters.

Written by Habib Baloch

I build Android apps, websites and Ubuntu servers, and write down exactly how I did it.

Ask me about this guide →