Elevate Flutter PWA with a Seamless Offline Experience using Workbox Caching
Optimising Offline Performance of Flutter PWAs with Workbox Caching
Flutter Progressive Web Apps (PWAs) have gained significant popularity due to their ability to provide a seamless native user experience across various devices and platforms. In this blog, we'll explore how to optimize the offline performance and capabilities of your Flutter PWA through Workbox caching. Let's begin with the brief note on Flutter PWA.
Flutter PWA
A Flutter PWA is essentially a web app created using the Flutter framework that comes with a built-in service worker setup to serve basic capabilities. It combines the power of Flutter's UI capabilities with the reach and accessibility of the web.
Service Workers are scripts that run in the background, intercepting network requests, and allowing you to control how resources are cached and served to your application.
How's Flutter PWA unique from traditional Javascript framework PWA's
Aspects | Flutter | Other Javascript Frameworks |
Rendering Engine | Flutter uses Canvaskit and HTML renderer | Relies on native rendering capabilities of web |
Service Worker | Follows a Flutter-specific approach to caching and asset management as it involves its rendering engines | Various popular techniques are available to simplify the configuration process as it uses conventional web engine |
Workbox | Workbox can still be used with Flutter PWA but some use cases still seem to be tricky because of the unique requirements of the framework | Workbox is commonly used for service worker configuration, offering a wide range of caching strategies |
PWA Setup | The process of converting a Flutter web app into a PWA involves a unique set of configurations and dependencies that are specific to Flutter | JavaScript framework PWAs have established practices and tooling for PWA setup. |
Why is Caching Important?
Caching is crucial for PWAs because it enables applications to function seamlessly even when there's no internet connection. It allows the app to load previously visited pages or resources from the user's device, reducing loading times and providing uninterrupted usability.
Disadvantages of Flutter's Built-in Service Worker Setup
Limited customization options and control for caching strategies
Less flexibility for advanced scenarios.
This is where Workbox comes into play over Flutter's built-in service worker setup.
Workbox Caching
Workbox is a powerful library that simplifies service worker management and caching strategies.
Need
Workbox provides flexibility, advanced caching strategies, and efficient precaching, enhancing your Flutter PWA's offline capabilities.
It simplifies cache management, and cache cleanup and integrates with existing infrastructure.
Now that we've covered the basics, Workbox provides different techniques for configuration, among them we will explore the InjectManifest
technique in this blog since it provides better control over other workbox caching options, especially in implementing advanced caching strategies.
Let's take a basic counter app in Flutter with a built-in service-worker setup as an example and add the required workbox and service-worker configurations to improve the offline functionality of the app.
To start with let's create a workbox-config.js
in the web folder with the below configuration:
module.exports = {
globDirectory: "build/web/",
globPatterns: ["**/*.{json,otf,ttf,js,wasm,png,html}"],
swSrc: "web/service-worker.js",
swDest: "build/web/service-worker.js",
maximumFileSizeToCacheInBytes: 10000000,
};
Each parameter serves a specific purpose for configuring the behaviour of Workbox in a service worker.
globDirectory: "build/web/"
- Specifies the directory where the service worker should look for files to cache. In our case, it's thebuild/web
directory.globPatterns: ["**/*.{json,otf,ttf,js,wasm,png,html}"]
- Defines an array of patterns for files to cache. It uses globs to match file names. In our case, file extensions are JSON, OTF, TTF, JS, WebAssembly (WASM), PNG, and HTML as these cover all types of files in thebuild/web
directory.swSrc: "web/service-worker.js"
- Specifies the path to the source file of the service worker. This is the original service worker script that will be used as a basis for generating the final service worker.swDest: "build/web/service-worker.js"
- Sets the destination path for the generated service worker file. The final service worker will be created and stored in thebuild/web
directory.maximumFileSizeToCacheInBytes: 10000000
- Defines the maximum file size, in bytes, for files to be cached. Files larger than this size will not be included in the cache. In this case, it's set to 10,000,000 bytes (10 MB). Files larger than this threshold are typically excluded from the cache to prevent excessively large files from being cached.
Now we are done with the workbox config file that can be used to generate the final service worker with caching configuration. Let's start with the service worker setup.
Create a
service-worker.js
in the web folder and Import the Workbox library, specifically the service worker module using CDN.importScripts( "https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js" );
Use
precacheAndRoute
function which takes in the path of the files to be precached on the initial load of the app.const { precacheAndRoute } = workbox.precaching; CACHE_FILES = self.__WB_MANIFEST ?? []; precacheAndRoute(CACHE_FILES);
💡self.__WB_MANIFEST
is the placeholder for the array of files to be cached which will be added later during build by injecting workbox-config.js.Listen to the
install
event to ensure immediate activation of the service worker usingself.skipWaiting()
and enable the service worker to claim control of the clients if any old service workers exist usingself.clients.claim()
in theactivate
event.self.addEventListener("install", function (event) { self.skipWaiting(); }); self.addEventListener("activate", () => { return self.clients.claim(); });
To override Flutter's built-in service worker setup, Add
serviceWorkerUrl
to the configuration in the Flutter loader'sloadEntrypoint
function (index.html
).window.addEventListener('load', function(ev) { // Download main.dart.js _flutter.loader.loadEntrypoint({ serviceWorker: { serviceWorkerUrl: './service-worker.js', serviceWorkerVersion: serviceWorkerVersion, }, onEntrypointLoaded: function(engineInitializer) { engineInitializer.initializeEngine().then(function(appRunner) { appRunner.runApp(); }); } }); });
Now we are all set in terms of workbox and service worker. Let's build our Flutter web app without Flutter's builtin service worker setup using
--pwa-strategy none
option.flutter build web --pwa-strategy none --release
Then, Install the Workbox CLI with Node, run the following command in your terminal
npm install workbox-cli --global
Run workbox inject manifest to populate the array of URLs to be cached that matches the glob pattern in place of
self.__WB_MANIFEST
in the destination service worker file in thebuild/web
directory.workbox injectManifest web/workbox-config.js
You can observe in the above log, that the service worker has added 26 URLs to precache in place of
self.__WB_MANIFEST
in the destination service worker filebuild/web/service-worker.js
.To test the offline functionality of our app locally, we can use
http-server
npm package to serve ourbuild/web
artifact directory, install it using the following command in your terminalnpm install http-server --global
Run the below command in your terminal from the root directory of the app to serve the build artifact of our Flutter web app.
http-server build/web/
This will serve the app in the listed URLs with the port specified as shown in the below log, we can use the same to access our app in the local browser.
Storing Data in Offline
Offline storage is one of the key factors to improve the user's overall offline experience, there are several offline storage options for web apps the choice may vary in terms of storage capacity, features, and browser support.
💡It's important to note that the storage capacity and browser support may change over time. Therefore, it's recommended to check the latest documentation and support status for each offline storage option.Let's use
localStorage
in our app to store the_counter
, to avoid data loss when the user goes offline or while refreshing the app. You can see the current logic of the counter in the below code.int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; });
To use
localStorage
in Flutter we can use theshared_preferences
package, to add it open thepubspec.yaml
file and add the following line under dependencies. Then runflutter pub get
to download and install the package.dependencies: flutter: sdk: flutter shared_preferences: ^2.2.2
In
main.dart
, let's import and initialise the_counter
with the value of thecounter
key in the local storage if not exists with 0.import 'package:shared_preferences/shared_preferences.dart';
Once we have an instance of
SharedPreferences
, we can use it to store data using theset
method.int _counter = 0; late SharedPreferences prefs; @override void initState() { super.initState(); // Load the counter value from SharedPreferences when the widget initializes. _loadCounter(); } void _loadCounter() async { prefs = await SharedPreferences.getInstance(); setState(() { _counter = prefs.getInt('counter') ?? 0; }); } void _incrementCounter() async { setState(() { _counter++; }); // Save the updated counter value to SharedPreferences. prefs.setInt('counter', _counter); }
Now the
_counter
will persist offline, even after refresh and between app sessions ensuring no data loss and giving users, the confidence to continue using the app without worrying about any external factors.
Other Suggestions:
Though we have done a basic configuration of service-worker with the help of workbox for flutter PWAs, workbox provides a lot of other options in terms of customising network strategies. Feel free to explore further on that.
Considering caching assets, especially images, it is better to use SVGs than PNGs for a seamless offline experience and to cache any other type of files in the app, we can straightaway include that in the globPattern of workbox config.
Checkout the live version of the app with offline capabilities on workbox-flutter-pwa.netlify.app
Github Repository: Mohanraj-Muthukumaran/Workbox-Flutter-PWA
Final Thoughts
In this blog, we highlighted how Workbox enhances cache management, allowing PWAs to function offline effectively over built-in service worker setups. To implement this we discussed InjectManifest
technique, which provides precise control over caching. Additionally, we covered the importance of offline data storage to ensure data persistence even offline.
So, whether you're building a Flutter PWA for a personal project or a business application, integrating Workbox caching and offline data storage is a strategic move that will set your app apart. Your users will appreciate the reliability and smooth performance, making it a win-win for both developers and their audiences.