Flutter: Conditional Compilation for Web

One of the big issues with Flutter for Web right now is it’s lack of support for dart.io. This means things like a simple Platform.isAndroid call will cause your web builds to crash on startup. In fact, just including the dart.io package _at all_ will break your app completely.

In cases like this, what is needed is some form of conditional compilation, so we can include the code on some platforms, and exclude it on others.

Some platforms like Unity, have built in platform defines, which let you easily partition sections of code for specific platforms. Unfortunately, this is not so easy to do with Flutter, but it is possible!

Currently, the best way (that we’ve found) to do this in Flutter, is by using several files, and a global function that gets overridden.

Universal Platform Detection

To demonstrate how this works, we’ll build an extremely simply plugin, that lets us detect Platform safely on all platforms. If you’d like to follow along with the actual code, you can check it out here: https://pub.dev/packages/universal_platform

First, a look at the structure we’re building, 3 files, with 1 acting as the main entry point or facade for the plugin:

The first step is to define a global function that will be overridden by each platform specific implementation. In our case, we’re going to have the default implementation return web, and we define this in the _locator file:

//Default to web, the platform_io class will override this if it gets imported.
UniversalPlatformType get currentUniversalPlatform => UniversalPlatformType.Web;

Next we’ll build the class that acts as the main facade for the underlying API. It makes calls on the global function that we just defined in the locator:

class UniversalPlatform {

  static bool get isWeb => currentUniversalPlatform == UniversalPlatformType.Web;
  static bool get isMacOS => currentUniversalPlatform == UniversalPlatformType.MacOS;
  static bool get isWindows => currentUniversalPlatform == UniversalPlatformType.Windows;
  static bool get isLinux => currentUniversalPlatform == UniversalPlatformType.Linux;
  static bool get isAndroid => currentUniversalPlatform == UniversalPlatformType.Android;
  static bool get isIOS => currentUniversalPlatform == UniversalPlatformType.IOS;
  static bool get isFuchsia => currentUniversalPlatform == UniversalPlatformType.Fuchsia;

}

enum UniversalPlatformType {
  Web,
  Windows,
  Linux,
  MacOS,
  Android,
  Fuchsia,
  IOS
}

So, now that we have our standard API defined, this is where things get a bit tricky!

We add a conditional import to the top of the main file, like so:

import 'src/universal_platform_locator.dart' if(dart.library.io) 'src/platform_io.dart';

abstract class UniversalPlatform {
...
}

enum UniversalPlatformType {
...
}

Looks closely at the import, and you can start to see what is happening. We’re going to import the _locator no matter what, and then, only if dart.library.io is available, will we import the platform_io.dart class which contains our volatile dart.io reference.

So what does platform_io.dart look like? It simply imports dart.io and overrides the global function, replacing it completely:

import 'dart:io';

//Override default method, to provide .io access
UniversalPlatformType get currentUniversalPlatform {
  if(Platform.isWindows) return UniversalPlatformType.Windows;
  if(Platform.isFuchsia) return UniversalPlatformType.Fuchsia;
  if(Platform.isMacOS) return UniversalPlatformType.MacOS;
  if(Platform.isLinux) return UniversalPlatformType.Linux;
  if(Platform.isIOS) return UniversalPlatformType.IOS;
  return UniversalPlatformType.Android;
}

Now, when your app makes calls on UniversalPlatform.isAndroid on web, it’s going to return the results of the platform_locator.currentUniversalPlatform() global function, but when you make the same call on Desktop/Mobile, you’re going to get the results of platform_io.currentUniversalPlatform()

And just like that (kidding), you have conditional compilation in dart!

Taking it further…

In this case we were implementing a static class, but often you will instead want to create multiple class instances. You do this by creating an Abstract Class in your facade, and using a factory constructor + global function to trick the Facade into returning the right class type for your platform.

We can look at this quickly with some pseudo-code:

/////////////////////////////////////////////////////////
//foo.dart, Main facade, contains conditional imports, and factory constructor
/////////////////////////////////////////////////////////
import 'foo_locator.dart' if (dart.library.html) 'foo_web.dart' if (dart.library.io) 'foo_io.dart';

abstract class Foo {
  void doStuff();
  factory Foo() => getFoo();
}


/////////////////////////////////////////////////////////
//foo_locator.dart, contains un-implemented global function
/////////////////////////////////////////////////////////
Foo getFoo() => throw UnsupportedError('Cannot create an abstract Foo!');


/////////////////////////////////////////////////////////
// foo_web.dart, implements the abstract class, and overrides the global fxn
/////////////////////////////////////////////////////////
class FooWeb implements Foo {
   void doStuff() { // Do web stuff here }
}
Foo getFoo() => FooWeb(); //override global fxn to return Web


/////////////////////////////////////////////////////////
// foo_io.dart, implements the abstract class, and overrides the global fxn
/////////////////////////////////////////////////////////
class FooIo implements Foo {
   void doStuff() { // Do io stuff here }
}
Foo getFoo() => FooIo(); //override global fxn to return Io

Now, when the app calls new Foo(), which is an abstract class, it’s actually going to get one of the concrete implementations! This is because of the trick with the global override + factory constructor.

Hopefully that’s all making sense 🙂 Definitely involves a bit more boilerplate than we would like, but it’s not really too bad once you get used to it. If you have any questions, or develop an extension of your own, please let us know in the comments below!

shawn.blais

Shawn has worked as programmer and product designer for over 20 years, shipping several games on Steam and Playstation and dozens of apps on mobile. He has extensive programming experience with Dart, C#, ActionScript, SQL, PHP and Javascript and a deep proficiency with motion graphics and UX design. Shawn is currently the Technical Director for gskinner.

@tree_fortress

Leave a Reply

Your email address will not be published. Required fields are marked *