DEV Community

Giovani Fouz
Giovani Fouz

Posted on

WebVirt + Nexus: Run Your React/Vue/Svelte SPA Inside an Android WebView

WebVirt + Nexus: Run Your React/Vue/Svelte SPA Inside an Android WebView — No Capacitor, No Server, No Permissions

TL;DR — Two decoupled Android libraries: one serves your SPA from APK assets like a real web server, the other bridges JavaScript to native code. Used together in a production app serving local businesses. Open source, JitPack-ready. Engine v3 and Nexus v2 coming soon.


The Problem Nobody Talks About

You built a great SPA in React + Vite + TypeScript. It runs beautifully at localhost:5173. Now your client wants it packaged as an Android app.

Your options:

Approach Reality
Capacitor / Cordova +25MB runtime, opinionated structure, black-box WebView
file:// protocol CORS broken, SPA routes 404, APIs blocked
Embedded HTTP server (NanoHTTPD) Threads, ports, network permissions, overkill
Raw WebView You write everything from scratch

What you actually want: your SPA thinks it's on https://app.local, everything comes from assets/, zero server, zero permissions, zero overhead.

That's exactly what WebVirt does.


What Is WebVirt?

WebVirt is a Java Android library that intercepts WebViewClient.shouldInterceptRequest() and serves your SPA assets directly from the APK — no actual server involved.

WebVirt.with(this)
    .host("app.local")
    .bind(webView);

webView.loadUrl("https://app.local/");
Enter fullscreen mode Exit fullscreen mode

Your React app loads. Routes work. CORS works. No permissions needed.

Already published:


Real Production Use Case

WebVirt v1 + Nexus are currently running in a financial management app used by small local businesses. The tech stack:

  • ⚛️ React + Vite + TypeScript + Tailwind CSS v4
  • 📦 ~5MB of assets served entirely from APK
  • 📋 Product lists of 90+ items — fast scrolling, no lag
  • 📤 JSON export (native)
  • 📥 JSON import with file picker (no permissions)
  • 📄 PDF export (native)
  • 🔒 Restrictive CSP
  • ⚡ Near-instant load time

No internet required. No server running. Just assets + native bridges.


What's Coming: WebVirt Engine v3 + Nexus v2

The production experience revealed what was missing. Two new libraries are in the works:

WebVirt Engine — The Production-Grade Upgrade

Built on the same shouldInterceptRequest() approach but adds what real apps need:

Smart caching with LRU + ETags

First load:  index.html → read → ETag generated → cached (LRU)
Second load: index.html → ETag match → 304 Not Modified → 0 bytes transferred
Enter fullscreen mode Exit fullscreen mode

Real Range Requests for video streaming

Client:  GET /video.mp4   Range: bytes=1048576-2097151
Server:  206 Partial Content
         Content-Range: bytes 1048576-2097151/52428800
         [only the requested bytes — file never fully loaded into RAM]
Enter fullscreen mode Exit fullscreen mode

Security headers on every response

Content-Security-Policy: default-src 'self'; script-src 'self'...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Enter fullscreen mode Exit fullscreen mode

Virtual API routes

WebVirt.with(this)
    .host("app.local")
    .route("/api/config", req -> json("{\"version\": \"1.0\"}"))
    .cspPolicy("default-src 'self'; connect-src https://api.example.com")
    .bind(webView);
Enter fullscreen mode Exit fullscreen mode

The architecture uses Chain of Responsibility for request routing and Strategy for path handlers — the same GoF patterns you'd use in a backend framework, applied to Android's WebView layer.


Nexus — The Native Bridge

Nexus is a completely separate library for JavaScript-to-native communication. It doesn't know WebVirt exists. WebVirt doesn't know Nexus exists.

Nexus nexus = Nexus.installOn(webView)
    .withDebugMode(false)
    .withGlobalTimeout(30_000)
    .registerHandler("export", new ExportHandlerAdapter(handler))
    .registerHandler("import", new ImportHandlerAdapter(context))
    .initialize()
    .withFilePicker(activity);

nexus.attachToWebViewLifecycle();
Enter fullscreen mode Exit fullscreen mode

From JavaScript:

const result = await window.__nexus.call("export", { format: "json" });
Enter fullscreen mode Exit fullscreen mode

What makes Nexus interesting:

Smart re-injection — The JS runtime re-injects only on real page navigations, not on React Router hash changes (#/products, #/settings). This was a real pain point discovered in production.

// From the actual source
String baseUrl = url.contains("#") 
    ? url.substring(0, url.indexOf("#")) 
    : url;

if (!baseUrl.equals(previousBaseUrl)) {
    previousBaseUrl = baseUrl;
    notifyPageLoaded(); // Only re-inject here
}
Enter fullscreen mode Exit fullscreen mode

FilePicker without permissions — Uses Android's Storage Access Framework (SAF). No READ_EXTERNAL_STORAGE in your manifest.

nexus.withFilePicker(activity); // That's it. No permissions.
Enter fullscreen mode Exit fullscreen mode

Interceptor chain — Add cross-cutting concerns (logging, auth, rate limiting) without touching your handlers:

Nexus.installOn(webView)
    .registerInterceptor(new AuthInterceptor())
    .registerInterceptor(new LoggingInterceptor())
    .registerHandler("export", new ExportHandler())
    .initialize();
Enter fullscreen mode Exit fullscreen mode

Event emission from native to JS:

nexus.emitEvent("syncComplete", Map.of("records", 42));
Enter fullscreen mode Exit fullscreen mode
window.__nexus.on("syncComplete", (data) => {
    console.log(`Synced ${data.records} records`);
});
Enter fullscreen mode Exit fullscreen mode

The Architecture: Two Libraries That Don't Know Each Other

This is the part I'm most proud of.

┌─────────────────────────────────────────────────────┐
│                      WebView                        │
│                                                     │
│  ┌─────────────────────────────────────────────┐   │
│  │         WebVirt Engine                       │   │
│  │  shouldInterceptRequest() → assets/          │   │
│  │  "app.local/index.html" → real index.html    │   │
│  │  CSP, ETags, Range Requests, LRU Cache      │   │
│  └─────────────────────────────────────────────┘   │
│                    ▲                                │
│                    │ Decorates (doesn't override)   │
│  ┌─────────────────────────────────────────────┐   │
│  │         Nexus WebViewLifecycleObserver       │   │
│  │  Observes onPageFinished, onPageStarted      │   │
│  │  Delegates everything else to WebVirt        │   │
│  └─────────────────────────────────────────────┘   │
│                                                     │
│  ┌─────────────────────────────────────────────┐   │
│  │         Nexus JSInterface (parallel channel) │   │
│  │  window.__nexus.call("export", data)         │   │
│  │  Does NOT go through shouldInterceptRequest  │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The key design decision: WebViewLifecycleObserver wraps WebVirt's client using the Decorator pattern. It adds lifecycle hooks without overriding or replacing WebVirt's request handling.

WebVirt doesn't know... Nexus doesn't know...
That Nexus exists That WebVirt exists
There's a JS bridge How assets are served
What handlers are registered What virtual host is used

This is actual decoupling, not just marketing copy.


Honest Comparison

Feature WebVirt Engine Capacitor Raw WebView
Runtime size 0KB overhead ~25MB 0KB
Smart cache (LRU + ETags)
Range Requests (video) ✅ Real Delegates to system
Configurable CSP
SPA route fallback ✅ Automatic
Offline mode ✅ Built-in
Hot reload (dev)
Native JS bridge With Nexus Manual
No permissions needed
Interceptor chain ✅ (Nexus) N/A
Setup time ~5 min ~2 hours Variable

Who Should Use This

✅ React/Vue/Svelte developers who need Android without Capacitor's complexity

✅ Small teams who want full control with minimal dependencies

✅ Offline-first apps (no internet required after install)

✅ Apps where clean architecture matters

❌ If you need live hot reload during development

❌ If your team is already deep in the Capacitor ecosystem


What's Available Now vs. Coming Soon

Status
WebVirt v1.0.0 ✅ GitHub + JitPack
Nexus (production) ✅ Running in production
WebVirt Engine v3.1.1 🔜 Coming soon — github.com/fouzstack/webvirt-engine
Nexus v2.0.0 🔜 Coming soon — github.com/fouzstack/nexus
Kotlin DSL (-ktx modules) 🔜 Planned
Unit tests + benchmarks 🔜 Planned

When Engine and Nexus launch, I'll post a follow-up with concrete benchmarks: cold start times, cache hit rates, memory footprint.


Try WebVirt Now

// settings.gradle
dependencyResolutionManagement {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

// build.gradle (app)
dependencies {
    implementation 'com.github.fouzstack:fouzstack-webvirt:1.0.0'
}
Enter fullscreen mode Exit fullscreen mode
WebVirt.with(this)
    .host("app.local")
    .bind(webView);

webView.loadUrl("https://app.local/");
Enter fullscreen mode Exit fullscreen mode

Put your React/Vite build output in assets/ and you're done.


Questions, feedback, or contributions welcome.

What native handler would you find most useful with Nexus — camera access, biometrics, Bluetooth, local database? Let me know in the comments, it'll help prioritize what gets documented first.


Built by @fouzstack — open source Android libraries for the WebView ecosystem.

Top comments (0)