Convert a PWA into a Flutter App using WebViews

Learn how to convert a PWA to a Flutter app using Flutter’s InAppWebView 6 plugin

photo by olaf val Feather unsplash

In this article, we are going to convert a PWA (Progressive Web App) in a Flutter mobile app for Android and iOS using the latest version 6 of flutter_inappwebview Placement

Progressive web app is a term that refers to web applications that are developed and loaded like regular web pages but that behave in the same way as a native application when used on a mobile device.

They are built and enhanced with modern APIs to provide superior scalability, reliability, and installability while being accessible to anyone, anywhere, on any device with a single codebase. Progressive web applications take advantage of this dynamism of the new web as well as technologies such as service workers and appear to provide a native app-like user experience that works even when the user is offline.

Developers can publish the web application online, ensure that it meets basic installation requirements, and users can add the application to their home screens. Publishing the app to a digital distribution system such as the Apple App Store or Google Play is optional.

Hybrid apps are applications that combine features of both native apps and web apps. They move inside a container, in this case, a WebView,

They’re available through the App Store, access native APIs and hardware components of your phone, and are installed on your device just like a native app.

I will not explain the pros and cons between PWAs, Native Apps and Hybrid Apps as it is beyond the scope of this article. You can already find it on the web.

As PWA example, we will use (GitHub repo: js13kpwa), which is a fully functional PWA with offline support.

js13kpwa Here is a list of A-Frame entries submitted to the js13kGames 2017 contest, which are used as an example for MDN articles about Progressive Web Apps. js13kPWA has an app shell architecture, works offline with a service worker, is installable thanks to the manifest file and add to homescreen feature, and is reconfigurable using notifications and push.

Furthermore, for this use case, we will add a simple two-way communication between JavaScript and Flutter/Dart.

service worker

Service workers are a fundamental part of a PWA. They enable faster loading (regardless of network), offline access, push notifications, and other capabilities.

the inspection For JavaScript Service Worker API availability based on WebView/browser version.

Service workers are available on Android starting with “Android 5–6.x WebView: Chromium 107” and on iOS starting with iOS 14.0+.

On iOS, enabling the Service Worker API requires additional setup using an app-bound domain (read Webkit – App-Bound Domain article for more details).

The App-Bound Domains feature takes steps to preserve user privacy by limiting the domains on which apps can use powerful APIs to track users during in-app browsing.

You can specify up to ten “app-bound” domains using Info.plist key WKAppBoundDomains,

So, we need to add our PWA’s domain to it. Otherwise, the Service Worker API will not work. In our use case, we need to add mdn.github.io workspace. here is an example ios/Runner/Info.plist file:



WKAppBoundDomains

mdn.github.io


internet network detection

It is important to detect whether the user’s mobile phone is connected to the Internet or not. WebView To load PWAs from cache instead of requesting online resources.

To check whether there is a valid connection, i.e. cellular network or Wi-Fi, we will use connectivity_plus Placement Instead, to check whether the network is connected to the Internet, we can try to look up the host address, like https://example.com/,

Here is the full code intro:

Future isNetworkAvailable() async 
// check if there is a valid network connection
final connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult != ConnectivityResult.mobile &&
connectivityResult != ConnectivityResult.wifi)
return false;

// check if the network is really connected to Internet
try result[0].rawAddress.isEmpty)
return false;

on SocketException catch (_)
return false;

return true;

InAppWebView basic settings

For making InAppWebView work correctly, we need to set some basic settings:

InAppWebViewSettings(
// enable opening windows support
supportMultipleWindows: true,
javaScriptCanOpenWindowsAutomatically: true,

// useful for identifying traffic, e.g. in Google Analytics.
applicationNameForUserAgent: 'My PWA App Name',
// Override the User Agent, otherwise some external APIs, such as Google and Facebook logins, will not work
// because they recognize the default WebView User Agent.
userAgent:
'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.105 Mobile Safari/537.36',

disableDefaultErrorPage: true,

// enable iOS service worker feature limited to defined App Bound Domains
limitsNavigationsToAppBoundDomains: true
);

Change it based on your needs.

In this example, we are enabling support for multiple windows if we want to open a popup WebView windows.

In some cases, you may need to override the user agent to a different value than the default to be able to use some external APIs, such as Google and Facebook login. Otherwise, they will not work because they recognize and block the default WebView Consumer Agent.

Additionally, you must set limitsNavigationsToAppBoundDomains setting to true Enable Service Worker API on iOS.

HTTP (non-HTTPS) support

Starting with Android 9 (API level 28), cleartext support is disabled by default:

On iOS, you need to disable apple transport safety (ATS) facility. There are two options:

  • Disable ATS for specific domain only (official wiki): (add the following code to your Info.plist file)
NSAppTransportSecurity

NSExceptionDomains

www.yourserver.com


NSIncludesSubdomains


NSTemporaryExceptionAllowsInsecureHTTPLoads


NSTemporaryExceptionMinimumTLSVersion
TLSv1.1


  • Completely disabled ATS (official wiki, add the following code to your Info.plist file:
NSAppTransportSecurity

NSAllowsArbitraryLoads

other useful Info.plist Properties are:

  • NSAllowsLocalNetworking: A boolean value indicating whether to allow local resources to be loaded (official wiki,
  • NSAllowsArbitraryLoadsInWebContent: A Boolean value indicating whether all App Transport security restrictions are disabled for requests made from web views (official wiki,

Also, we’re going to use WidgetsBindingObserver For Android, it’s useful to know when the system puts an app in the background or returns an app to the foreground.

With it, we can pause and resume JavaScript execution and safely pause any processing such as video, audio, and animations.

here is a simple implementation didChangeAppLifecycleState,

@override
void didChangeAppLifecycleState(AppLifecycleState state)
if (!kIsWeb)
if (webViewController != null &&
defaultTargetPlatform == TargetPlatform.android)
if (state == AppLifecycleState.paused)
pauseAll();
else
resumeAll();



void pauseAll()
if (defaultTargetPlatform == TargetPlatform.android)
webViewController?.pause();

webViewController?.pauseTimers();

void resumeAll()
if (defaultTargetPlatform == TargetPlatform.android)
webViewController?.resume();

webViewController?.resumeTimers();

To detect Android back button click, we wrap our main Scaffold a in the widget app WillPopScope Widget and implement onWillPop way back in the history of WebView,

Here is an implementation example:

@override
Widget build(BuildContext context)
return WillPopScope(
onWillPop: () async
// detect Android back button click
final controller = webViewController;
if (controller != null)
if (await controller.canGoBack())
controller.goBack();
return false;


return true;
,
child: Scaffold(
appBar: AppBar(
// remove the toolbar
toolbarHeight: 0,
),
body: // ...
),
);

Before loading the PWA URL inside InAppWebView Wrapping up, we check if internet connection is available using previously defined utility, and based on that, we need to set cache mode and policy for Android and iOS like this:

// Android-only
final cacheMode = networkAvailable
? CacheMode.LOAD_DEFAULT
: CacheMode.LOAD_CACHE_ELSE_NETWORK;

// iOS-only
final cachePolicy = networkAvailable
? URLRequestCachePolicy.USE_PROTOCOL_CACHE_POLICY
: URLRequestCachePolicy.RETURN_CACHE_DATA_ELSE_LOAD;

cacheMode will be used in initialSettings property and cachePolicy will be used in URLRequest Of initialUrlRequest Property.

This logic allows us to load cached data if there is an unavailable internet connection.

To limit navigation to PWA hosts only, we implement shouldOverrideUrlLoading method to check if a specific HTTP request to the key frame does not match the PWA host, so we will open that request in the third party app url_launcher Put:

shouldOverrideUrlLoading:
(controller, navigationAction) async
// restrict navigation to target host, open external links in 3rd party apps
final uri = navigationAction.request.url;
if (uri != null &&
navigationAction.isForMainFrame &&
uri.host != kPwaHost &&
await canLaunchUrl(uri))
launchUrl(uri);
return NavigationActionPolicy.CANCEL;

return NavigationActionPolicy.ALLOW;
,

To detect whether the PWA is “installed” correctly the first time, we implement onLoadStop WebView Method to check availability of internet connection and if PWA is already installed:

onLoadStop: (controller, url) async 
if (await isNetworkAvailable() && !(await isPWAInstalled()))
// if network is available and this is the first time
setPWAInstalled();

,

two utilities, isPWAInstalled And setPWAInstalledcan be implemented as follows using shared_preferences Plugin to get and save PWA installation status:

Future isPWAInstalled() async 
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isInstalled') ?? false;

void setPWAInstalled(bool installed = true) async
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isInstalled', installed);

All these utilities allow us to detect the network availability and installation status of the PWA so that we can implement a custom error page, as shown below:

onReceivedError: (controller, request, error) async 
final isForMainFrame = request.isForMainFrame ?? true;
if (isForMainFrame && !(await isNetworkAvailable()))
if (!(await isPWAInstalled()))
await controller.loadData(
data: kHTMLErrorPageNotInstalled);


,

Where kHTMLErrorPageNotInstalled is a string containing our custom HTML.

if you need support web notification javascript apiUnfortunately, Android-native WebView and iOS-native WKWebView don’t natively support that feature, so we must implement it ourselves! For an example implementation, you can check web notification project example, it uses a UserScript To inject custom JavaScript code on web page startup to implement the Web Notifications API.

The injected JavaScript code tries to create a “polyfill” for it. Notification Communicate using the Window object and Flutter/Dart side by side javascript handler To manage and implement related notification UI, for example, when you are requesting permission Notification.requestPermission() Or when you want to show a notification.

Also, if you need to support camera and microphone usage (for example, a WebRTC app), you need to implement onPermissionRequest event and ask for permission to use, for example, permission_handler Placement For more information, visit Official WebRTC Guide And this WebRTC Project Example,

To handle requests that open a new window using JavaScript (window.open()) or by the target attribute in a link (eg target="_blank"), we must implement onCreateWindow event and return true To announce that we are handling the request. Here is a simple example:

onCreateWindow: (controller, createWindowAction) async 
showDialog(
context: context,
builder: (context)
final popupWebViewSettings =
sharedSettings.copy();
popupWebViewSettings.supportMultipleWindows =
false;
popupWebViewSettings
.javaScriptCanOpenWindowsAutomatically =
false;

return WebViewPopup(
createWindowAction: createWindowAction,
popupWebViewSettings: popupWebViewSettings);
,
);
return true;
,

WebViewPopup is the second InAppWebView instance within a AlertDialog widget that takes input createWindowAction to get windowId to use for new WebView, windowId is an identifier that is used to obtain rights in the originating party WebView The context that flutter should show. WebView will also implement popup onCloseWindow To listen for when the popup should be closed and removed from the widget tree:

onCloseWindow: (controller) 
Navigator.pop(context);
,

also check popup window project example For an example implementation.

To implement our two-way communication between Javascript and Flutter/Dart side, we will use javascript handler Speciality

For our usage example, we want to listen for clicks on the “Request Dummy Notification” button HTML element with the id notifications show more SnackBar With random text generated by Javascript.

To do this, we make a simple user script and inject this after the page is loaded:

initialUserScripts: UnmodifiableListView([
UserScript(
source: """
document.getElementById('notifications').addEventListener('click', function(event)
var randomText = Math.random().toString(36).slice(2, 7);
window.flutter_inappwebview.callHandler('requestDummyNotification', randomText);
);
""",
injectionTime:
UserScriptInjectionTime.AT_DOCUMENT_END)
]),

Then, we add the corresponding JavaScript handler right after when WebView Example created:

onWebViewCreated: (controller) 
webViewController = controller;

controller.addJavaScriptHandler(
handlerName: 'requestDummyNotification',
callback: (arguments)
final String randomText =
arguments.isNotEmpty ? arguments[0] : '';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(randomText)));
,
);
,

Here is the result:

Android example result.
iOS example result.

Complete code project example is available here https://github.com/pichillilorenzo/flutter_inappwebview_examples/tree/main/pwa_to_flutter_app

that’s it for today!

Are you using this plugin? submit your app via submit app page and follow the instructions. Control performance page to see who is already using it!

This project follows all contributors Specialty (Contributors, I want to thank all the people who are supporting the project in any way. Many thanks to all of you!

This Post Has One Comment

Leave a Reply