Flutter web has been in beta since December 2019, and the most recent release introduced some performance improvements and enhanced the developer experience (full overview here).
More and more companies are using it to create great user experiences.
And if your already have a Flutter mobile app, you can port it to web with minimal effort and nearly 100% code reuse.
But there are a few things that you need to be aware of. So in this tutorial I'm sharing 8 top tips to save time in your Flutter web projects.
This list is by no means exhaustive, and mainly based on issues I encountered (and resolved) while using Flutter web in production in one of my projects.
To get started with Flutter web, make sure to follow the official docs.
On to the tips!
1. Use --web-enable-expression-evaluation
Debugging Flutter web apps in VSCode was not possible until recently, and we had to rely on the browser developer tools instead.
--web-enable-expression-evaluation
changes this, can be enabled in the .vscode/launch.json
file:
{
"version": "0.2.0",
"configurations": [
{
"name": "web",
"program": "lib/main.dart",
"request": "launch",
"type": "dart",
"args": [
"--web-port=5000",
"--web-enable-expression-evaluation"
],
},
]
}
Note: this is only supported on the flutter
dev
ormaster
branches.
Once you've done this, you can start debugging your Flutter web apps in VSCode, using breakpoints.
2. Develop on the iPad simulator
The setting above makes it easier to debug on web, but in my experience hot-reload is still a bit flaky.
By running on the iPad simulator I get a superior development experience, but with the same screen form factor as on web.
Of course, from time to time I still run on web to make sure everything works as expected. This is particularly important when testing platform-dependent code.
3. Enable Firebase Hosting for the project
Configuring and deploying any site with Firebase Hosting is simple, and the steps are well documented here:
If we use Firebase, we can add a new web app in the project settings page. Then, we need to add the correct Firebase SDK snippet to our app.
If we link our project to a Firebase Hosting site, a new "automatic" mode becomes available:
Without it, we have to configure our app manually via CDN by adding this to our index.html
file:
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js"></script>
<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#available-libraries -->
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "<your-api-key>",
authDomain: "<your-auth-domain>",
databaseURL: "<your-database-url>",
projectId: "<your-project-id>",
storageBucket: "<your-storage-bucket>",
messagingSenderId: "<your-messaging-sender-id>",
appId: "<your-app-id>",
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
By using the "Automatic" mode, the script above becomes much simpler:
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="/__/firebase/7.14.2/firebase-app.js"></script>
<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#available-libraries -->
<!-- Initialize Firebase -->
<script src="/__/firebase/init.js"></script>
The /__/
syntax above points to a reserved namespace that is enabled for Firebase Hosting websites. Read more details here:
4. Keep multiple `index.html` files for different flavors
Firebase Hosting is cool, but you'll still want to debug your app on localhost
, and for that you need to use the CDN-based setup.
Rather than constantly switching the Firebase SDK snippet in index.html
, we can keep multiple copies of it for different environments / flavors. This makes it easier to work on localhost, development and production.
To support this, we can add a web_flavors
folder alongside the web
one:
web/
index.html
web_flavors/
localhost/
index.html
development/
index.html
production/
index.html
Then, we can copy the correct index.html
file into web
as needed.
For production builds, adding a simple shell script to the CI workflow should do:
# $flavor is an environment variable that is configured on a per-workflow basis
cp web_flavors/$flavor/index.html web/index.html
flutter build web --release
5. Handle Cross-Origin Resource Sharing (CORS)
If your web app makes requests to another domain (for example, where your Cloud Functions are hosted), you will encounter some XMLHttpRequest
errors when sending your requests.
To understand why this happens, we need to learn about CORS - Cross-Origin Resource Sharing.
This document on Handling CORS requests explains what CORS is and how to configure it. Quoting:
Cross-Origin Resource Sharing (CORS) is a way to let applications running on one domain access content from another domain, for example, letting
yourdomain.com
make requests toregion-project.cloudfunctions.net/yourfunction
. If CORS isn't set up properly, you're likely to get errors that look like this:
XMLHttpRequest cannot load https://region-project.cloudfunctions.net/function.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://yourdomain.com' is therefore not allowed access.
The steps for setting up CORS are explained in detail here, but they are a bit tedious, especially if your API supports multiple endpoints.
Luckily, handling CORS becomes painless if you use Firebase Hosting, and is just a case of setting up rewrite rules for your APIs.
Rewrite rules can be configured in the firebase.json
file, as explained here:
This will solve any CORS issues in your deployed web app.
But making requests in localhost
will still not work, making parts of your app unusable.
StackOverflow saves us here, and some workarounds for this are explained in "Disable same origin policy in Chrome". I could get this to work by opening Chrome with this command (on macOS):
open /Applications/Google\ Chrome.app --args --user-data-dir="/var/tmp/Chrome dev session" --disable-web-security
Once web security is disabled, we'll see this banner in Chrome:
After setting this, requests will work correctly on localhost
.
NOTE: This works, but we still need to open Chrome with --disable-web-security
every single time. If you know of a better way, let me know. 🙏
6. Enable copy-paste in your Flutter web app
Believe it or not, by default it's not possible to copy text inside a Flutter web app (go ahead and try it here: the text is not selectable at all).
Flutter offers a Clipboard.setData method, but this doesn't work on web. Luckily, this thread offers a workaround:
// copy_to_clipboard_web.dart
import 'dart:html';
// https://github.com/flutter/flutter/issues/33470#issuecomment-537802636
bool copyToClipboardImpl(String text) {
final textarea = TextAreaElement();
document.body.append(textarea);
textarea.style.border = '0';
textarea.style.margin = '0';
textarea.style.padding = '0';
textarea.style.opacity = '0';
textarea.style.position = 'absolute';
textarea.readOnly = true;
textarea.value = text;
textarea.select();
final result = document.execCommand('copy');
textarea.remove();
return result;
}
This works, but we won't be able to build this code on iOS/Android, where dart:html
is not available.
Worry not, as the next tip shows how to handle this.
7. Conditionally import files at compile time
Since dart:html
is only available on web, we need a way to define and import different files at compile time.
To enable this we need four different files.
One file (copy_to_clipboard_web.dart
) will be web-specific and contain the code from above.
A second file contains a non-web implementation (using the Clipboard API in this case):
// copy_to_clipboard_non_web.dart
import 'package:flutter/services.dart';
bool copyToClipboardImpl(String text) {
Clipboard.setData(ClipboardData(text: text));
return true;
}
A third file will be a placeholder defining a stub implementation that throws an UnsupportedError
:
// copy_to_clipboard_stub.dart
bool copyToClipboardImpl(String text) =>
throw UnsupportedError('No implementation for copyToClipboardImpl');
The last file puts everything together by importing the correct file depending on whether we're building for web or not:
// copy_to_clipboard.dart
import 'copy_to_clipboard_stub.dart'
if (dart.library.html) 'copy_to_clipboard_web.dart'
if (dart.library.io) 'copy_to_clipboard_non_web.dart';
bool copyToClipboard(String text) => copyToClipboardImpl(text);
The main idea here is to define three separate files with three implementations for the copyToClipboardImpl
function.
Then we can call copyToClipboard
when we want to copy some text.
Bottom-line: you can create different source files for web and non-web functionality, and use conditional imports to
import
the correct code at compile time.
This will ensure that you can build your Flutter app on web and non-web.
But is there a way to check if we're running on web at runtime?
Time for the last tip:
8. Check if we're running on web at runtime
We can use the Platform
class to check on which platform we're running on.
But Platform
is defined in 'dart:io'
and can't be used on web.
Instead, we can use the kIsWeb
constant from foundation.dart
:
/// A constant that is true if the application was compiled to run on the web.
///
/// This implementation takes advantage of the fact that JavaScript does not
/// support integers. In this environment, Dart's doubles and ints are
/// backed by the same kind of object. Thus a double `0.0` is identical
/// to an integer `0`. This is not true for Dart code running in AOT or on the
/// VM.
const bool kIsWeb = identical(0, 0.0);
This is how to use it:
import 'package:flutter/foundation.dart';
if (kIsWeb) {
print("we're on web");
} else {
print("we're not on web");
}
Wrap Up
Flutter web is a new exciting frontier. While Flutter is getting better at delivering a first-class experience on web, there are still some quirks and I hope these tips will be useful.
In addition these tips, I also recommend reading this article:
This includes more information about how Flutter web apps are now Progressive Web Apps by default. And also how you can enable CanvasKit rendering for superior perfomance.
Have you encountered any specific issues with Flutter web, or want to share more tips? Let me know on Twitter or by email.
Happy coding!