Stop using kIsWeb — The right way to implement multi-platform code in your Flutter project
When I started working with Flutter last year, I instantly loved how easy it was to get certain values like Platform.isAndroid
or Platform.isIos
directly from the dart:io
libray. Other things like kDebugMode
from the foundation constants also seemed to make work a lot easier.
After months of working on a mobile app for Android and iOS, the management decided to make it live on Web as well. By that time, it was a moderately sized project and was not made with the web platform in mind since the beginning.
As soon as we ran the project on web for the first time, we were greeted by this gentleman.
Unsupported operation: Platform._operatingSystem
So what does it mean?
After a couple of google searches I understood that the Platform
class, or worse; the whole dart:io
library was unsupported on web.
Because Platform
deals with different operating systems using method channels — it does not include web browsers.
Now the problem statement was: We had these platform checks at no less than a hundred places throughout the application.
We did what developers do: started looking around for everything from solutions to makeshifts.
What are the options?
Of course we were not the only ones going through this, we found various solutions on the internet — and each broke for a different use case.
Solution #1: Use a third-party package dependency
A package universal_io
which deals with the exact same issue as ours. Pretty neat and quick solution to the problem, but I did not like the idea of using a package just for checking platforms. It was indeed the last resort, but not the first.
Solution #2: Use kIsWeb
kIsWeb
is a bool from the foundation constants, and you can access it anywhere as a top-level declaration. The idea was to wrap the Platform
checks with a kIsWeb
check to ensure that the execution didn’t reach Platform
functions at runtime on web. Consider the example below:
import 'package:flutter/foundation.dart';
import 'dart:io';
if (kIsWeb) {
// do the web thing
} else {
if (Platform.isAndroid) {
// do the android thing
} else if (Platform.isIos) {
// do the ios thing
}
}
This seemed like a good solution at first, but with time we realised that there was a problem; actually two — For some cases, even just importing dart:io
crashed the web app. And second, we had to import dart:html
at several places for web implementations, which did not do good to mobile apps in a similar way. We were trying to keep two swords in one sheath.
Solution #3: Conditional imports (the multiplatform approach)
Ah yes, the winner! This one is a bit complicated and invites boilerplate, but you are going to love it. The idea is to keep the dart:io
and dart:html
logics in separate dart files and include the appropriate file in the project at compile time.
We do not have to write any script or use any dependency, it is actually provided by the Dart Team.
How do we do that?
For this example, let’s aim to create a function that would give us one of the following values based on the platform: android
ios
or web
Step #1: Creating an enum of possible platforms
// platform.dart
enum Platform { android, ios, web }
Step #2: Create a stub file
This will be the placeholder file for our multiplatform interface. It will not have any real implementation but will have all the function signatures. Consider it as the “else” of what happens if neither dart:io
or dart:html
are appropriate imports according to the Dart compiler. It also tells your IDE to calm down.
// multiplatform_stub.dart
Platform getPlatform() {
throw UnimplementedError('Unsupported');
}
Step #3: Actual implementations
In the multiplatform_io.dart
file, we will import dart:io
without hesitation knowing it will only be compiled for mobile apps.
// multiplatform_io.dart
import 'dart:io' as io;
Platform getPlatform() {
if (io.Platform.isAndroid) return Platform.android;
if (io.Platform.isIOS) return Platform.ios;
throw UnimplementedError('Unsupported');
}
Similarly, we may add web specific imports such as dart:html
and dart:js
in the multiplatform_web.dart
file. Note that we need to keep the function name and signature same as in the other file.
// multiplatform_web.dart
Platform getPlatform() {
return Platform.web;
}
Step #4: Conditional importing
Create a multiplatform.dart
file and add the following code
export 'multiplatform_stub.dart'
if (dart.library.io) 'multiplatform_io.dart'
if (dart.library.html) 'multiplatform_web.dart';
All that this file is doing, is exporting the appropriate file based on the platform (or library). To understand this properly, try to read it backwards:
- if the library is
html
, exportmultiplatform_web.dart
- if the library is
io
, exportmultiplatform_io.dart
- else, export
multiplatform_stub.dart
Note: As of the time writing this blog you cannot use any bool other than the dart.library.io
and dart.library.html
with the export/import statements, not even bool literals true
or false
.
Step #5: Using the implementation
To use it safely in your code, just import the multiplatform.dart
file like any other library or package, and start calling out functions, getters or values as top-level declarations.
// main.dart
import 'platform.dart'; // our enum
import 'multiplatform.dart'; // multiplatform will export code according to platform
void main() {
Platform platform = getPlatform(); // calling as top-level declaration
print(platform.name); // prints enum to string
}
That’s it. We just created a concrete implementation for all our platforms! You can use this for any platform-specific feature — even Widgets.
Limitations
I have observed two small limitations with this implementation:
- Absence of OOP — as the files are nowhere linked to each other, we cannot implement a common interface to ensure that all functions are being declared and overridden in all platform files.
- Needs care — while using, we have to make sure that we always import the
multiplatform.dart
file only, and not one of its allies. Importing the wrong file will result in crashes.
Conclusion
This was worth the struggle. After finding and implementing this approach I was able to understand the Dart compilation processes better while removing a major hurdle in future development scope. And of course we still use our beloved kIsWeb
when in need.
After this, you can be confident that your apps won’t crash on any existing or additional platforms in the future. Do try this and let me know your observations. Let’s meet in another blog. Happy coding!