A2HS in Flutter
Source code for establishing A2HS functionality in flutter web.
Please consider going through article about this.
Dev.To: Link
Medium: Link
Posted on August 8, 2021
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.
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:
Source: MDN Web Docs
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.
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):
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
We can already see icons folder there, just add appropriate icons there, and make sure to add them in the manifest file.
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:
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.
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.
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.
For showing prompt, we need to host web app to a secure HTTPS hosting. We'll host the web app on Github Pages.
flutter build web --web-renderer=html
build/web
directory.{username}.github.io
this repository.And now, you're all done! 🥂
To check visit: {username}.github.io
Things to keep in mind:
deferredPrompt is null
printed.beforeinstallprompt
callback. Click here to see.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! ❤️
Posted on August 8, 2021
Sign up to receive the latest update from our blog.