DEV Community

Роман Шамагин
Роман Шамагин

Posted on

Not Just a WebView: Building a Native Engine on Flutter to Convert Sites to Apps with SDUI

WebView wrappers suck. They are slow, offer poor UX, and get instantly rejected by Apple under Guideline 4.2 ("Minimum Functionality"). Usually, it's just a browser without an address bar.

I decided to solve this engineering challenge properly. My goal: a platform where WebView is just a content slot wrapped in a fully native Flutter UI.

In this article:

  1. Architecture: Linking JS and Dart via a two-way bridge.
  2. Server-Driven UI: Changing native controls (tabs, drawers) without recompiling.
  3. 5 Platforms, 1 Codebase: iOS, Android, macOS, Windows, Linux.
  4. Bypassing Apple Review: How we pass moderation by providing native permissions and screens.

No fluff, just architecture and code.


1. Why Flutter?

When building a "factory" for app generation, requirements were strict:

  1. Performance: No lag.
  2. Cross-platform: Clients need Desktop (Windows/macOS) for corporate portals, not just Mobile.

Flutter won because:

  • One codebase for 5 platforms. React Native is great for mobile, but desktop is tricky. Flutter builds .apk, .ipa, .exe, .dmg, and Linux binaries from one source.
  • Pixel Perfect. Skia/Impeller rendering ensures my native menus look identical everywhere.
  • Platform Channels. Robust bridge to native features (Payment, Push).

2. "Smart WebView" Architecture

My approach is a "Layered Cake", not a full-screen WebView.

      +---------------------------------------------------+
      |          LAYER 1: FLUTTER NATIVE SHELL            |
      | +-----------------------------------------------+ |
      | |  [=] App Bar             Bottom Nav Bar [..]  | |
      | |  Native Loaders          Native Permissions   | |
      | +-----------------------------------------------+ |
      +------------------------+--------------------------+
                               |
                   (Commands / Haptic / Push)
                               |
      +------------------------v--------------------------+
      |              LAYER 2: JS BRIDGE                   |
      |   [ Dart Code ]  <==========>  [ JavaScript ]     |
      +------------------------+--------------------------+
                               |
                    (Events / Auth Tokens)
                               |
      +------------------------v--------------------------+
      |               LAYER 3: WEBVIEW                    |
      | +-----------------------------------------------+ |
      | |  <html>                                       | |
      | |    Your Website (SPA/SSR)                     | |
      | |    React / Vue / Angular / WordPress          | |
      | |  </html>                                      | |
      | +-----------------------------------------------+ |
      +---------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Layer 1: Native Shell (Flutter)

Navigation happens in native code, not HTML:

  • Bottom Navigation Bar — Native Flutter widget. Instant tab switching.
  • AppBar / Drawers — Native.
  • Loaders & Errors — Native "No Connection" screens, not browser errors.

Layer 2: The JS Bridge

Scenario: User clicks "Buy" on the site.

  1. JS: AppLikeWeb.postMessage(JSON.stringify({event: 'purchase', value: 100}));
  2. Dart: Catches message via JavascriptChannel.
  3. Native Action: Haptic feedback, AppsFlyer event, In-App Review request.
JavascriptChannel(
  name: 'AppLikeWeb',
  onMessageReceived: (JavascriptMessage message) {
    final data = jsonDecode(message.message);
    switch (data['event']) {
      case 'purchase':
        AnalyticsService.trackPurchase(data['value']);
        HapticFeedback.mediumImpact();
        break;
    }
  },
)
Enter fullscreen mode Exit fullscreen mode

3. Killer Feature: Server-Driven UI (SDUI)

Mobile dev pain: Update Cycles. Changing a menu item color usually requires a Store Review (1-3 days).

I implemented SDUI. At launch, the app fetches a JSON config:

{
  "theme": { "primary_color": "#FF5733" },
  "navigation": {
    "type": "bottom_bar",
    "items": [
      { "id": "home", "icon": "home_outlined", "url": "https://mysite.com/" },
      { "id": "cart", "icon": "shopping_cart", "badge_source_js": "getCartCount()" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The app rebuilds widgets on the fly. I can disable ads on iOS or change the color scheme instantly. Zero days in review.

(See how our Design Customization works)

App Design Config


4. Fighting Apple Guideline 4.2

Apple hates "site wrappers". We solve this by giving users native control over Permissions:

  • Location / Camera / Mic: Managed via native screens.
  • Biometrics: FaceID/TouchID login.
  • Offline Mode: Native placeholders.

When a moderator sees native permission management and deep OS integration, the "it's just a website" argument dies. We recently released a dedicated update specifically for handling these permissions natively.

Permissions Management


5. Infrastructure: Building Hundreds of Apps

Manual Xcode builds are hell. AppLikeWeb automates this via CI/CD (Bash + Fastlane).

  1. Client clicks "Create App".
  2. CI Pipeline runs.
  3. Output: APK, IPA, DMG, EXE.
# 1. Generate assets
flutter pub run flutter_launcher_icons:main

# 2. Rename Bundle ID
dart run rename_app.dart --bundleId "com.client.$APP_ID"

# 3. Build & Upload
flutter build ipa --release
fastlane pilot upload --ipa build/ios/ipa/*.ipa
Enter fullscreen mode Exit fullscreen mode

APK Generation


6. Technical Pitfalls (Under the hood)

It wasn't all smooth sailing. Here are the top 3 issues I faced when mixing Flutter and WebView:

Issue #1: OAuth and "Disallowed User Agent"

Google blocks auth via WebView for security reasons. If a user clicks "Login with Google", they get a 403 error.
Solution:
We spoof the User-Agent on the fly to look like a mobile browser, or intercept the OAuth URL and open it in SFSafariViewController / ChromeCustomTabs, then pass the cookie back.

Issue #2: File Uploads on Android

On iOS, <input type="file"> works out of the box. On Android WebView, it ignores clicks by default.
Solution:
Override onShowFileChooser in WebChromeClient and manually trigger Flutter's native file picker, then pass the result back to WebView.

Issue #3: Syncing Cookies

If a user logs in via a native screen, the WebView doesn't know about it.
Solution:
Use CookieManager. Before loading the first page, I inject the auth token from SecureStorage into the WebView cookies.

Future<void> syncCookies(String url) async {
  final cookieManager = WebViewCookieManager();
  final token = await _storage.read(key: 'auth_token');

  if (token != null) {
    await cookieManager.setCookie(
      WebViewCookie(
        name: 'access_token',
        value: token,
        domain: Uri.parse(url).host,
        path: '/',
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

AppLikeWeb saves months of development for content/e-commerce projects. It's not for complex banking apps, but for 90% of businesses with a good mobile site, it's the fastest way to the Store with great UX.


Supported by:

  • MegaV.app — Fast and secure VPN for privacy.
  • PinVPS — High-performance Cloud VPS (NVMe, Ryzen) for your backend.

(Link to tool: https://applikeweb.com/)

Top comments (0)