A2HS in Flutter Web
Tags: flutter, pwa, webdev, javascript
Published: 2021-08-08 13:38:57

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](https://user-images.githubusercontent.com/47249618/128632990-d6230621-c54c-4b8b-8344-bb90a4f99908.gif)

------------

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](#flutter-implementation).

### 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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps#pwa_advantages)

### 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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen/fx-a2hs-banner.png)

Source & More Info: [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen)

**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](#manifest)
2. [Icons available](#icons)
3. [Service workers](#service-workers)
4. [A2HS Prompt Configuration](#a2hs-prompt)
5. [Show A2HS Prompt From Flutter Web App](#show-a2hs-prompt-with-flutter)
6. [HTTPS context](#final-step-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:
```json
     {
        "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/
    	    }
        ]
       }
```
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](https://i.imgur.com/G5x4wCw.png)

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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen#manifest)

### 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:
```javascript
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);
    })
  );
});
```

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:
```javascript
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);
        });
```
This will register our service worker we defined in `sw.js`

Source & More Info: 
1. [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
2. [Google Web Dev](https://developers.google.com/web/ilt/pwa/lab-scripting-the-service-worker)


### 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:
```javascript
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;
  }
}

```
`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](https://pub.dev/packages/universal_html) 

We will also require Shared Prefs, so add it too.
Link for package: [Shared Preferences](https://pub.dev/packages/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.
```dart
import  "package:universal_html/js.dart"  as js;
import  'package:flutter/foundation.dart'  show kIsWeb;
```
Import the two packages.
And now add the following code to the `initState`:
```dart
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.");
          }
        }
      });
    }

```

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:
```dart
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!"))
            ],
          ),
        ),
      );
    },
  );
}

```
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](https://caniuse.com/?search=beforeinstallprompt) 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**: {% github https://github.com/SahilSonawaneOfficial/flutter_web_a2hs %}



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