Flutter Web — “New website version is available, press Update to refresh”

Updates help to provide users with a better user experience; likewise, essential to deliver updates seamlessly to keep excellent UX…

Flutter Web — “New website version is available, press Update to refresh”

Updates help provide users with a better experience; likewise, delivering updates seamlessly is essential to maintain an excellent UX throughout the entire application lifecycle.

💡
Service Worker Generator had been introduced recently to properly handle updates

Simultaneously, seamless Flutter Web updates are still in progress: https://github.com/flutter/flutter/issues/104509.

For today, we can find our own solution to ensure seamless updates.

The most common way to update a Flutter application's version is to update the version number in the pubspec.yaml file. Surprisingly, Flutter continues loading old scripts from the browser cache after a new version of the Web application is deployed, causing issues. Sometimes, the application doesn’t work at all because the updated version still uses old scripts and APIs. Currently, you can solve these issues by clearing the browser cache or browsing in incognito mode — but these solutions rely on users, which makes the application UX worse.

The solution seems obvious — specify versions for script filenames according to application version: ‘flutter.js?v1.2.3’.

The current Flutter version is 3.7.5, and it uses 3 script files that we want to update to ensure the application is fully reloaded when the web page reloads: main.dart.js, flutter.js, and flutter_service_worker.js. We’ll take a closer look at these files a bit later.

When Flutter builds a web application, it stores the application version in the version.json file. This file is located in the web root folder, so we can easily query it and compare it to the version of the application we are running:

final updateVersionUri = Uri.base.removeFragment().replace(path: '/version.json');
final updateVersionResponse = await http.get(updateVersionUri);
final versionModel = VersionModel.fromJson(json.decode(updateVersionResponse.body));
final updateVersion = '${versionModel.version}b${versionModel.buildNumber}';

final packageInfo = await PackageInfo.fromPlatform();
final currentVersion = '${packageInfo.version}b${packageInfo.buildNumber}';

If the updateVersion is different from the currentVersion, we could display the SnackBar with the ‘Update’ button. As we already know, it won’t work out of the box for the current version of Flutter, so let’s update the script files mentioned above.

To update main.dart.js and flutter.js script files we are using simple Dart program prepare_app_to_start.dart:

// Path to the web root
const webRoot = '/usr/share/nginx/html';
// Locations of the Web application files we are going to update
const indexHtmlPath = '$webRoot/index.html';
const flutterJsPath = '$webRoot/flutter.js';
const versionJsonPath = '$webRoot/version.json';

// Compose app version by adding build_number
final versionJsonFile = File(versionJsonPath);
final versionJson = jsonDecode(await versionJsonFile.readAsString());
final appVersion = '${versionJson['version']}b${versionJson['build_number']}'.trim();

// Update flutter.js file by specifying app version to the main.dart.js
final flutterJsFile = File(flutterJsPath);
final flutterJsContent = await flutterJsFile.readAsString();
final flutterJsReplaced = flutterJsContent.replaceAll('main.dart.js', 'main.dart.js?$appVersion');
await flutterJsFile.writeAsString(flutterJsReplaced);

// Update index.html file by specifying app version to the flutter.js file
final indexHtmlFile = File(indexHtmlPath);
final indexHtml = await indexHtmlFile.readAsString();
final indexHtmlReplaced = indexHtml.replaceAll('"flutter.js"', '"flutter.js?$appVersion"');
await indexHtmlFile.writeAsString(indexHtmlReplaced);

Running a Flutter Web application as a Docker container looks more interesting and offers many extra opportunities. Let’s look at this case first.

Our Dockerfile consists of 3 parts. First, we are compiling a prepare-app-to-start utility from the Dart program prepare_app_to_start.dart described above:

FROM dart:stable AS build_dart
WORKDIR /app
COPY ./tool/ ./tool/
RUN dart compile exe tool/prepare_app_to_start.dart -o tool/prepare-app-to-start

Then we are building the Flutter Web application itself:

FROM plugfox/flutter:stable-web AS build_web
WORKDIR /home
COPY . .
RUN flutter pub get
RUN flutter pub run build_runner build --delete-conflicting-outputs --release
RUN flutter build web --release --no-source-maps \
    --no-tree-shake-icons --pwa-strategy offline-first \
    --web-renderer canvaskit --base-href /

Finally, we are creating fresh image and coping files that have been prepared in advance:

FROM nginx:alpine as production

COPY --from=build_dart /runtime/ /
COPY --from=build_dart /app/tool/prepare-app-to-start /app/bin/
COPY --from=build_dart --chmod=0755 /app/tool/entrypoint.sh /app/bin/
COPY --from=build_web /home/build/web /usr/share/nginx/html

EXPOSE 80/tcp
ENTRYPOINT ["/app/bin/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

The final image runs entrypoint.sh on startup to run the prepare-app-to-start utility described above. The ENTRYPOINT command runs during container startup, so we could use extra functionality and ENV variables at this point; we will look at the benefits of the Docker ENV variables in the next article.

#!/bin/sh

/app/bin/prepare-app-to-start app_environment=$APP_ENVIRONMENT app_version=$APP_VERSION

# Exec the CMD from the Dockerfile
exec "$@"

entrypoint.sh script runs the prepare-app-to-start utility and then executes the ‘nginx -g daemon off;’ command passed as a script parameter from the Dockerfile.

The Flutter Web application Docker image could be built and deployed using GitHub Actions or any other CI/CD. You can find a GitHub Actions example in the example project repository.

If you prefer Firebase CI/CD or build-time environment variables suit your needs— you could use ‘-dart-define’ parameters to the ‘flutter build’ command.

A Flutter Web application could check the http://{app.domain}/version.json version via an update menu button or periodically, or alternatively, subscribe to a gRPC service stream to be notified of updates.

A valuable advantage of this solution is that we update the Flutter scripts only once at container startup, which does not affect the application website’s performance, unlike adding additional startup scripts to index.html.

This approach is only a temporary workaround and proof of concept. It is not a production-ready solution. Native Flutter Web seamless updates are definitely preferable, while this is only a temporary solution to the Flutter issue. Furthermore, Flutter’s scripts change regularly, so be prepared to adapt this approach as future Flutter updates roll out.

Sample project with additional functionality published to the GitHub repository: https://github.com/zs-dima/web-app-update

I hope you find this workaround useful for updating Flutter Web applications and that this article helps the Flutter team understand the importance of seamless updates for Web applications.


🚀 Try It

web-app-update is open-source and ready-to-use. Go build something amazing.

📚 Further Reading

🤝 Let's Connect

I'm a software engineer building high-performance systems. If you have questions or want to clarify details, feel free to reach out.

Blog · GitHub · LinkedIn · X / Twitter · Email

with ❤️ Dmitrii Zusmanovich