DEV Community

Cover image for Building a Fully-Featured Custom WebView App in Android: Complete Guide
K-kibet
K-kibet

Posted on

Building a Fully-Featured Custom WebView App in Android: Complete Guide

Let us create a fully custom WebView app with advanced features in Java. Here's a comprehensive implementation:

1. Main Activity (MainActivity.java)

package com.example.customwebview;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.*;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

public class MainActivity extends AppCompatActivity {

    private AdvancedWebView webView;
    private EditText urlBar;
    private ProgressBar progressBar;
    private ImageButton btnBack, btnForward, btnRefresh, btnHome;
    private SwipeRefreshLayout swipeRefreshLayout;
    private LinearLayout bottomNavigation;
    private TextView pageTitle;

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

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

        initializeViews();
        setupWebView();
        setupEventListeners();

        // Load home page
        webView.loadUrl(HOME_URL);
    }

    private void initializeViews() {
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        webView = findViewById(R.id.webview);
        urlBar = findViewById(R.id.url_bar);
        progressBar = findViewById(R.id.progress_bar);
        btnBack = findViewById(R.id.btn_back);
        btnForward = findViewById(R.id.btn_forward);
        btnRefresh = findViewById(R.id.btn_refresh);
        btnHome = findViewById(R.id.btn_home);
        swipeRefreshLayout = findViewById(R.id.swipe_refresh);
        bottomNavigation = findViewById(R.id.bottom_navigation);
        pageTitle = findViewById(R.id.page_title);
    }

    @SuppressLint("SetJavaScriptEnabled")
    private void setupWebView() {
        WebSettings webSettings = webView.getSettings();

        // Enable basic features
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDomStorageEnabled(true);
        webSettings.setDatabaseEnabled(true);
        webSettings.setAppCacheEnabled(true);

        // Enable advanced features
        webSettings.setLoadWithOverviewMode(true);
        webSettings.setUseWideViewPort(true);
        webSettings.setBuiltInZoomControls(true);
        webSettings.setDisplayZoomControls(false);
        webSettings.setSupportZoom(true);
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
        webSettings.setAllowFileAccess(true);
        webSettings.setAllowContentAccess(true);

        // Performance optimizations
        webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
        webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);
        webSettings.setEnableSmoothTransition(true);

        // Security settings
        webSettings.setSavePassword(false);
        webSettings.setSaveFormData(false);

        // Set WebView client and chrome client
        webView.setWebViewClient(new CustomWebViewClient());
        webView.setWebChromeClient(new CustomWebChromeClient());

        // Enable mixed content for modern websites
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }
    }

    private void setupEventListeners() {
        // URL bar listener
        urlBar.setOnKeyListener((v, keyCode, event) -> {
            if ((event.getAction() == KeyEvent.ACTION_DOWN) && 
                (keyCode == KeyEvent.KEYCODE_ENTER)) {
                String url = urlBar.getText().toString().trim();
                if (!url.startsWith("http://") && !url.startsWith("https://")) {
                    url = "https://" + url;
                }
                webView.loadUrl(url);
                return true;
            }
            return false;
        });

        // Navigation buttons
        btnBack.setOnClickListener(v -> {
            if (webView.canGoBack()) {
                webView.goBack();
            }
        });

        btnForward.setOnClickListener(v -> {
            if (webView.canGoForward()) {
                webView.goForward();
            }
        });

        btnRefresh.setOnClickListener(v -> webView.reload());

        btnHome.setOnClickListener(v -> webView.loadUrl(HOME_URL));

        // Swipe to refresh
        swipeRefreshLayout.setOnRefreshListener(() -> {
            webView.reload();
            new Handler(Looper.getMainLooper()).postDelayed(() -> 
                swipeRefreshLayout.setRefreshing(false), 2000);
        });
    }

    private class CustomWebViewClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            String url = request.getUrl().toString();

            // Handle external URLs (tel, mailto, etc.)
            if (url.startsWith("tel:") || url.startsWith("mailto:") || 
                url.startsWith("sms:") || url.startsWith("market:")) {
                try {
                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    startActivity(intent);
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            // Block certain URLs if needed
            if (isBlockedUrl(url)) {
                view.loadUrl("about:blank");
                return true;
            }

            return super.shouldOverrideUrlLoading(view, request);
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            progressBar.setVisibility(View.VISIBLE);
            urlBar.setText(url);
            updateNavigationButtons();
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            progressBar.setVisibility(View.GONE);
            swipeRefreshLayout.setRefreshing(false);
            updateNavigationButtons();

            // Update page title
            String title = view.getTitle();
            if (title != null && !title.isEmpty()) {
                pageTitle.setText(title);
            }
        }

        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            super.onReceivedError(view, errorCode, description, failingUrl);
            progressBar.setVisibility(View.GONE);

            // Load custom error page
            String errorHtml = getErrorHtml(description, errorCode);
            view.loadDataWithBaseURL(null, errorHtml, "text/html", "UTF-8", null);
        }

        @SuppressWarnings("deprecation")
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            super.onReceivedError(view, request, error);
            if (request.isForMainFrame()) {
                progressBar.setVisibility(View.GONE);
            }
        }
    }

    private class CustomWebChromeClient extends WebChromeClient {
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            super.onProgressChanged(view, newProgress);
            progressBar.setProgress(newProgress);

            if (newProgress == 100) {
                progressBar.setVisibility(View.GONE);
            } else {
                progressBar.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onReceivedTitle(WebView view, String title) {
            super.onReceivedTitle(view, title);
            if (title != null && !title.isEmpty()) {
                pageTitle.setText(title);
            }
        }

        // Handle JavaScript alerts
        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
            // You can implement custom alert dialog here
            return super.onJsAlert(view, url, message, result);
        }
    }

    private void updateNavigationButtons() {
        btnBack.setEnabled(webView.canGoBack());
        btnForward.setEnabled(webView.canGoForward());

        // Change button appearance based on state
        btnBack.setAlpha(webView.canGoBack() ? 1.0f : 0.5f);
        btnForward.setAlpha(webView.canGoForward() ? 1.0f : 0.5f);
    }

    private boolean isBlockedUrl(String url) {
        // Add URLs you want to block
        String[] blockedPatterns = {
            "malicious.com",
            "phishing.site"
        };

        for (String pattern : blockedPatterns) {
            if (url.contains(pattern)) {
                return true;
            }
        }
        return false;
    }

    private String getErrorHtml(String description, int errorCode) {
        return "<html>" +
                "<body style='text-align:center; padding:50px;'>" +
                "<h2>Page Load Error</h2>" +
                "<p>Error Code: " + errorCode + "</p>" +
                "<p>" + description + "</p>" +
                "<button onclick='window.history.back()'>Go Back</button>" +
                "<button onclick='location.reload()'>Retry</button>" +
                "</body></html>";
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // Handle back button
        if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.menu_clear_cache) {
            clearCache();
            return true;
        } else if (id == R.id.menu_settings) {
            // Open settings activity
            return true;
        } else if (id == R.id.menu_share) {
            sharePage();
            return true;
        } else if (id == R.id.menu_bookmarks) {
            // Open bookmarks
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void clearCache() {
        webView.clearCache(true);
        CookieManager.getInstance().removeAllCookies(null);
        WebStorage.getInstance().deleteAllData();
    }

    private void sharePage() {
        String currentUrl = webView.getUrl();
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType("text/plain");
        shareIntent.putExtra(Intent.EXTRA_TEXT, currentUrl);
        startActivity(Intent.createChooser(shareIntent, "Share URL"));
    }

    @Override
    protected void onPause() {
        super.onPause();
        webView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        webView.onResume();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (webView != null) {
            webView.destroy();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Advanced WebView Class (AdvancedWebView.java)

package com.example.customwebview;

import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;

public class AdvancedWebView extends WebView {

    public AdvancedWebView(Context context) {
        super(context);
        initialize();
    }

    public AdvancedWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    public AdvancedWebView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize();
    }

    private void initialize() {
        // Additional initialization if needed
    }

    // Add custom methods here
    public void clearAllData() {
        clearCache(true);
        clearFormData();
        clearHistory();
        clearMatches();
    }

    public String getPageTitle() {
        return getTitle();
    }

    public String getCurrentUrl() {
        return getUrl();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Layout File (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- Toolbar -->
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="?attr/actionBarTheme">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="8dp">

            <TextView
                android:id="@+id/page_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="WebView Browser"
                android:textColor="@android:color/white"
                android:textSize="14sp"
                android:textStyle="bold" />

            <EditText
                android:id="@+id/url_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Enter URL or search"
                android:background="@android:color/white"
                android:padding="8dp"
                android:textSize="12sp"
                android:singleLine="true"
                android:imeOptions="actionGo" />

        </LinearLayout>

    </androidx.appcompat.widget.Toolbar>

    <!-- Progress Bar -->
    <ProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="4dp"
        android:visibility="gone" />

    <!-- Swipe Refresh Layout -->
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <!-- WebView -->
        <com.example.customwebview.AdvancedWebView
            android:id="@+id/webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <!-- Bottom Navigation -->
    <LinearLayout
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="#f5f5f5"
        android:padding="8dp">

        <ImageButton
            android:id="@+id/btn_back"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:src="@android:drawable/ic_media_previous"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Back" />

        <ImageButton
            android:id="@+id/btn_forward"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:src="@android:drawable/ic_media_next"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Forward" />

        <ImageButton
            android:id="@+id/btn_refresh"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:src="@android:drawable/ic_menu_rotate"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Refresh" />

        <ImageButton
            android:id="@+id/btn_home"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:src="@android:drawable/ic_menu_always_landscape_portrait"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Home" />

    </LinearLayout>

</LinearLayout>
Enter fullscreen mode Exit fullscreen mode

4. Menu Resource (main_menu.xml)

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

    <item
        android:id="@+id/menu_share"
        android:title="Share"
        android:icon="@android:drawable/ic_menu_share"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/menu_bookmarks"
        android:title="Bookmarks"
        android:icon="@android:drawable/ic_menu_agenda"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/menu_clear_cache"
        android:title="Clear Cache" />

    <item
        android:id="@+id/menu_settings"
        android:title="Settings" />

</menu>
Enter fullscreen mode Exit fullscreen mode

5. AndroidManifest.xml

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

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
        android:usesCleartextTraffic="true">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- Handle URL intents -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="http" />
                <data android:scheme="https" />
            </intent-filter>
        </activity>

    </application>

</manifest>
Enter fullscreen mode Exit fullscreen mode

6. Additional Features Class (WebViewFeatures.java)

package com.example.customwebview;

import android.webkit.WebView;
import android.webkit.JavascriptInterface;

public class WebViewFeatures {

    private WebView webView;

    public WebViewFeatures(WebView webView) {
        this.webView = webView;
        setupJavaScriptInterface();
    }

    private void setupJavaScriptInterface() {
        // Add JavaScript interface for bidirectional communication
        webView.addJavascriptInterface(new WebAppInterface(), "Android");
    }

    public class WebAppInterface {
        @JavascriptInterface
        public void showToast(String message) {
            // You can show toast messages from JavaScript
        }

        @JavascriptInterface
        public String getUserAgent() {
            return webView.getSettings().getUserAgentString();
        }
    }

    // Inject custom CSS
    public void injectCSS(String css) {
        String js = "javascript:(function() {" +
                "var parent = document.getElementsByTagName('head').item(0);" +
                "var style = document.createElement('style');" +
                "style.type = 'text/css';" +
                "style.innerHTML = '" + css + "';" +
                "parent.appendChild(style)" +
                "})()";
        webView.loadUrl(js);
    }

    // Inject custom JavaScript
    public void injectJavaScript(String js) {
        webView.loadUrl("javascript:" + js);
    }

    // Enable/disable JavaScript
    public void setJavaScriptEnabled(boolean enabled) {
        webView.getSettings().setJavaScriptEnabled(enabled);
    }

    // Set user agent
    public void setUserAgent(String userAgent) {
        webView.getSettings().setUserAgentString(userAgent);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Features Included:

  1. Custom Navigation: Back, forward, refresh, and home buttons
  2. URL Bar: With automatic URL formatting
  3. Progress Indicator: Shows page loading progress
  4. Swipe to Refresh: Pull down to refresh current page
  5. Error Handling: Custom error pages with retry functionality
  6. Security Features: URL blocking and safe browsing
  7. Cache Management: Clear cache and cookies
  8. Share Functionality: Share pages with other apps
  9. JavaScript Interface: Bidirectional communication
  10. Customizable Settings: Zoom, JavaScript, cache modes

Usage Instructions:

  1. Create a new Android project
  2. Replace the main activity with the provided code
  3. Add the layout files and menu resources
  4. Add internet permission to manifest
  5. Build and run

This implementation provides a solid foundation for a custom WebView browser with advanced features that you can further extend based on your specific requirements.

Top comments (0)