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!
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:
- Secure Network (HTTPS)
- Service Workers
- 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.
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):
- Have a manifest file
- Icons available
- Service workers
- A2HS Prompt Configuration
- Show A2HS Prompt From Flutter Web App
- 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/
}
]
}
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:
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);
})
);
});
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);
});
This will register our service worker we defined in sw.js
Source & More Info:
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;
}
}
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.
- Recheck manifest tab there, there should be no error or warning there.
- 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;
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.");
}
}
});
}
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!"))
],
),
),
);
},
);
}
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.
- Create a new repository, named "{username}.github.io"
- Run
flutter build web --web-renderer=html
- After successful build, navigate to
build/web
directory. - Initialize a new git repository and add remote to it. For
{username}.github.io
this repository. - 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:
That's all. This is my first article, I will love to hear suggestions to improve. Thanks! ❤️
Top comments (2)
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
Thanks Aditya, I'll definitely try to make more stuff like this!