DEV Community

Cover image for A2HS in Flutter Web
Sahil Sonawane
Sahil Sonawane

Posted on

A2HS in Flutter Web

When I wanted to implement Add to home screen feature in an application I was working in flutter, I didn't found much good solutions out there and I struggled a bit coming up with a solution.

In this article, I've described my personal solution to this. Please let me know if we can do this in a great way than this. Enjoy learning!

We're trying to achieve:
a2hs-demo-video


To start learning about A2HS (Add to Home Screen), we first need to learn about PWAs. Know this already? you can skip to the main content.

PWA (Progressive Web App):

PWAs or Progressive Web Apps are the web apps that use the cutting edge web browser APIs to bring native app-like user experience.
But how do we differentiate normal and PWA web app. It's simple we just need to check if it contains the following features:

  1. Secure Network (HTTPS)
  2. Service Workers
  3. Manifest File

Source: MDN Web Docs

A2HS:

What's A2HS?
Add to Home screen (or A2HS for short) is a feature available in modern browsers that allows a user to "install" a web app, ie. add a shortcut to their Home screen representing their favorite web app (or site) so they can subsequently access it with a single tap.

A2HS browser example

Source & More Info: MDN Web Docs

Relation of A2HS with PWA?
As we learnt, A2HS's job is to provide you ability to install the web app on your device. Therefore, it needs the web app to have offline functionality.
Therefore, PWAs quite fit for this role.


Flutter Implementation

Well, now that we've learned, what PWA and A2HS means, let's now get to the main point, i.e. creating A2HS functionality to flutter web app or creating flutter PWA.

Let's first make the Flutter Web App, Flutter PWA.
Create a new flutter app (web enabled) and go through the steps below.

For this, we want to (click on link to navigate to the section):

  1. Have a manifest file
  2. Icons available
  3. Service workers
  4. A2HS Prompt Configuration
  5. Show A2HS Prompt From Flutter Web App
  6. HTTPS context

Manifest

Particular:
The web manifest is written in standard JSON format and should be placed somewhere inside your app directory. It contains multiple fields that define certain information about the web app and how it should behave. To know more about fields, checkout the source docs.

Implementation:
Flutter web comes with a manifest.json file already but some of the browsers don't support it. Therefore, we'll create a new file in web root directory named, "manifest.webmanifest" .
Add this code in it:

     {
        "name": "FlutterA2HS",
        "short_name": "FA2HS",
        "start_url": ".",
        "display": "standalone",
        "background_color": "#0175C2",
        "theme_color": "#0175C2",
        "description": "Flutter A2HS Demo Application",
        "orientation": "portrait-primary", 
        "prefer_related_applications": false,
        "icons": [
            {
            "src": "icons/Icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
            },
            {
            "src": "icons/Icon-512.png",
            "sizes": "512x512",
            "type": "image/
            }
        ]
       }
Enter fullscreen mode Exit fullscreen mode

Add this line in the head tag of your index.html file:
<link rel="manifest" href="manifest.webmanifest">

Run the app and navigate to Dev Tools > Application > Manifest.
You should see this:

Manifest Tab in Web Dev Tools

If you see some warning, please consider resolving them.

Note: All the fields here are required for PWA to work. Please consider replacing values in it. Though you can reduce the number of images in icons list.

Source & More Info: MDN Web Docs

Icons

We can already see icons folder there, just add appropriate icons there, and make sure to add them in the manifest file.

Service Workers

Particular:
Service workers essentially act as proxy servers that sit between web applications, the browser, and the network (when available). They are intended, among other things, to enable the creation of effective offline experiences, intercept network requests and take appropriate action based on whether the network is available, and update assets residing on the server. They will also allow access to push notifications and background sync APIs.

Implementation:
Create a file named "sw.js" in root folder where manifest belongs.

Add following code there:

const cacheName = "flutter-app-cache-v1";
const assetsToCache = [
  "/",
  "/index.html",
  "/icons/Icon-192.png",
  "/icons/Icon-512.png",
];

self.addEventListener("install", (event) => {
  self.skipWaiting(); // skip waiting
  event.waitUntil(
    caches.open(cacheName).then((cache) => {
      return cache.addAll(assetsToCache);
    })
  );
});

self.addEventListener("fetch", function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      // Cache hit - return response
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

This will cache network urls and assets.

The service worker emits an install event at the end of registration. In the above code, a message is logged inside the install event listener, but in a real-world app this would be a good place for caching static assets.

Now,
In in index.html before the default service worker registration of flutter (above line: var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;).
Add the following code:

var customServiceWorkerUrl = './sw.js';
        navigator.serviceWorker.register(customServiceWorkerUrl, { scope: '.' }).then(function (registration) {
          // Registration was successful
          console.log('CustomServiceWorker registration successful with scope: ', registration.scope);
        }, function (err) {
          // registration failed 
          console.log('CustomServiceWorker registration failed: ', err);
        });
Enter fullscreen mode Exit fullscreen mode

This will register our service worker we defined in sw.js

Source & More Info:

  1. MDN Web Docs
  2. Google Web Dev

A2HS Prompt

Particular:
At last we're here, we now need to present the install dialog to user.
But now, an important issue here is, it will only prompt on event fire. For eg. on click event. So for eg. if you have a button in your html let's say, you'll fire a js onclickevent to call a function and show the prompt and bad part is it does not work automatically. But worry not, we'll get to this.

Implementation:
Create a script.js file in the root directory where manifest belongs and add the following code:

let deferredPrompt;

// add to homescreen
window.addEventListener("beforeinstallprompt", (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;
});

function isDeferredNotNull() {
  return deferredPrompt != null;
}

function presentAddToHome() {
  if (deferredPrompt != null) {
    // Update UI to notify the user they can add to home screen
    // Show the prompt
    deferredPrompt.prompt();
    // Wait for the user to respond to the prompt
    deferredPrompt.userChoice.then((choiceResult) => {
      if (choiceResult.outcome === "accepted") {
        console.log("User accepted the A2HS prompt");
      } else {
        console.log("User dismissed the A2HS prompt");
      }
      deferredPrompt = null;
    });
  } else {
    console.log("deferredPrompt is null");
    return null;
  }
}

Enter fullscreen mode Exit fullscreen mode

beforeinstallprompt will be called automatically when browser is ready to show prompt when A2HS conditions are fulfilled.

Now the idea is when beforeinstallprompt fires, it will populate defferredPrompt and we can then present the prompt.

Add this line in the head tag of index.html file: <script src="script.js" defer></script>

At this point, we've to check if all things are configured properly.
Run the app in browser and open developer tools (inspect) and navigate to application tab.

  1. Recheck manifest tab there, there should be no error or warning there.
  2. There should be no error or warning on the service worker tab too.

If there's no problem, then congratulations 🥳. We're all set with configurations, now we just need to call the prompt from our flutter app.

Show A2HS Prompt With Flutter

The concern here now is, how do we fire a JS callback from a button in flutter app let's say?

For this, now, we're going to use universal_html package. We can also do it with dart:js, but it's not recommended for using in flutter apps directly.
So go ahead and add universal_html as dependency in your pubspec.yaml file.
Link for package: Universal HTML

We will also require Shared Prefs, so add it too.
Link for package: Shared Preferences

We've to create a button to allow user to click and show the prompt. We'll for this eg. show a popup to user whenever it's ready to show prompt.
In main.dart file, we've the good-old counter app.

import  "package:universal_html/js.dart"  as js;
import  'package:flutter/foundation.dart'  show kIsWeb;
Enter fullscreen mode Exit fullscreen mode

Import the two packages.
And now add the following code to the initState:

if (kIsWeb) {
      WidgetsBinding.instance!.addPostFrameCallback((_) async {
        final _prefs = await SharedPreferences.getInstance();
        final _isWebDialogShownKey = "is-web-dialog-shown";
        final _isWebDialogShown = _prefs.getBool(_isWebDialogShownKey) ?? false;
        if (!_isWebDialogShown) {
          final bool isDeferredNotNull =
              js.context.callMethod("isDeferredNotNull") as bool;

          if (isDeferredNotNull) {
            debugPrint(">>> Add to HomeScreen prompt is ready.");
            await showAddHomePageDialog(context);
            _prefs.setBool(_isWebDialogShownKey, true);
          } else {
            debugPrint(">>> Add to HomeScreen prompt is not ready yet.");
          }
        }
      });
    }

Enter fullscreen mode Exit fullscreen mode

Here, we first check if the platform is web, if yes, then call the isDeferredNotNull function we wrote in script.js file. This will return us, if the defferredPrompt is not null (as we know this will only be not null when the browser is ready to show prompt.
If it's not null, then show the dialog and set the shared pref key to true to not show again.

Below is the dialog (popup) code:

Future<bool?> showAddHomePageDialog(BuildContext context) async {
  return showDialog<bool>(
    context: context,
    builder: (context) {
      return Dialog(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        child: Padding(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Center(
                  child: Icon(
                Icons.add_circle,
                size: 70,
                color: Theme.of(context).primaryColor,
              )),
              SizedBox(height: 20.0),
              Text(
                'Add to Homepage',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
              ),
              SizedBox(height: 20.0),
              Text(
                'Want to add this application to home screen?',
                style: TextStyle(fontSize: 16),
              ),
              SizedBox(height: 20.0),
              ElevatedButton(
                  onPressed: () {
                    js.context.callMethod("presentAddToHome");
                    Navigator.pop(context, false);
                  },
                  child: Text("Yes!"))
            ],
          ),
        ),
      );
    },
  );
}

Enter fullscreen mode Exit fullscreen mode

This will call the presentAddToHome function in the script.js to show the install prompt.

Final Step: HTTPS Context

For showing prompt, we need to host web app to a secure HTTPS hosting. We'll host the web app on Github Pages.

  1. Create a new repository, named "{username}.github.io"
  2. Run flutter build web --web-renderer=html
  3. After successful build, navigate to build/web directory.
  4. Initialize a new git repository and add remote to it. For {username}.github.io this repository.
  5. Push and wait for some time, check for the deployment status on the repository on GitHub.

And now, you're all done! 🥂

To check visit: {username}.github.io

Important:

Things to keep in mind:

  • Prompt will sometimes not be shown for the first time. Most probably it would be shown the next time you visit the page or reload the page. Please check it's terms. You can check the console, tab of the dev tools, if it's not ready you can see deferredPrompt is null printed.
  • Please see the supported browsers for beforeinstallprompt callback. Click here to see.
  • Try in different browser if not working on one, for eg. Mozilla Firefox, Brave, etc.
  • Will only work when hosted. Make sure you have no errors or warning on manifest in Applications tab in browser dev tools.

Hope you got the result you wanted!

Source Code:

A2HS in Flutter

a2hs-demo-video

Source code for establishing A2HS functionality in flutter web.
Please consider going through article about this.

Dev.To: Link
Medium: Link

That's all. This is my first article, I will love to hear suggestions to improve. Thanks! ❤️

Discussion (2)

Collapse
adityag2048 profile image
AdityaG2048

thanks for the highly intuitive yet sophisticated walkthrough on the topic, a amusingly informative read indeed. fascinating how the web is evolving with PWAs and so is flutter web. the tutorial would be a instrumental source of documentation for anyone wanting to create a PWA, by letting others build upon this stuff, it's a boon :D

Collapse
iamsahilsonawane profile image
Sahil Sonawane Author

Thanks Aditya, I'll definitely try to make more stuff like this!