Stop using kIsWeb — The right way to implement multi-platform code in your Flutter project

Chahat Gupta
5 min readJun 4, 2023

--

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, export multiplatform_web.dart
  • if the library is io, export multiplatform_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:

  1. 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.
  2. 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!

--

--

Chahat Gupta
Chahat Gupta

Written by Chahat Gupta

Mobile Tech Lead specialising in Android, iOS, and Flutter. Sharing insights and learnings on mobile development to inspire and elevate tech professionals.

Responses (2)