Gonçalo Palma
October 22, 2021

Conditional Importing - How to compile for all platforms in Flutter

Photo by Vincent van Zalinge on Unsplash.

One of the main reasons that everyone chooses Flutter is the ability to create hybrid applications that can work in multiple platforms with the same codebase. For the most part, this is easy to achieve - we use UI elements such as forms and buttons and perform REST API calls with the http package, which won’t make a difference for Web or Mobile. However, there comes the time when we need to use something that requires a platform interaction. Maybe it is the use of GeoLocation, or even integrating Google Maps into our apps and this is where it starts to get tricky.

As always, we use pub.dev to search for new packages, which gives us invaluable information on which platform each package can be used.

For example, the provider package by Remi Rousselet is available for all Flutter platforms, but it isn’t available for Native Dart:

provider package

On the other hand, Riverpod, also by Remi, is available for all Flutter platforms plus Dart native and Dart for Web:

riverpod package

This means that we must be careful when choosing a package to use in our apps. If we are targeting Android, iOS and Web, the package must accept all three platforms.

If we need to integrate a camera plugin for both mobile and web, we have different libraries we can choose from, but we need to always check for platform compatibility. For example, camera_camera does not support Flutter Web:

camera_camera package

But the camera plugin does:

camera_camera package

But why is this?

dart:io vs dart:html

Before diving into the technical details, we can think of the platforms and devices themselves.

All of (as far as I know) smartphone devices have a Gyroscope inside it that let the device know about the current orientation of the device. This means that when we are developing an application, we will be able to see if the device is in portrait mode, landscape mode or even face-down at the top of a table.

But the same can’t be applied to a laptop (or even a desktop!) computer. We don’t see people tilting their MacBooks to change the orientation of their laptop (although the devices do have accelerometers, it’s mainly used for protection of the disk drive). So fundamentally, there are uses that one device has that the other doesn’t have, which in part justifies why some mobile plugins don’t work for the web.

The other reason is the platform itself.

Flutter Mobile and Desktop are installed in the device itself, meaning that we can do I/O (input/output) operations such as reading, writing and creating local files. On the other hand, Flutter Web is going to be tied to an HTML document and has direct access to JavaScript, allowing certain operations such as navigating to other websites, setting cookies and changing the DOM.

Fundamentally, they are different, and in reality they rely on different core libraries:

Unfortunately, this means that if an app uses both dart:io and dart:html at the same time, we will be able to run the application but we will have the following runtime error message:

Error: Unsupported operation: _Namespace
    at Object.throw_ [as throw] (http://localhost:50052/dart_sdk.js:5061:11)
    at Function.get _namespacePointer [as _namespacePointer] (http://localhost:50052/dart_sdk.js:53920:17)
    at Function._namespacePointer (http://localhost:50052/dart_sdk.js:51823:28)
    at Function._dispatchWithNamespace (http://localhost:50052/dart_sdk.js:51826:31)
    at io._File.new.open (http://localhost:50052/dart_sdk.js:51941:23)
    at io._File.new.readAsBytes (http://localhost:50052/dart_sdk.js:52087:19)
    at createIo (http://localhost:50052/packages/awesome_app/io.dart.lib.js:16:34)
    at createIo.next (<anonymous>)

Or at compilation time for Flutter Mobile:

Failed to build iOS app
Error output from Xcode build:
↳
    ** BUILD FAILED **


Xcode's output:
↳
    lib/main.dart:2:8: Error: Not found: 'dart:html'
    import 'dart:html' as html;
           ^
    lib/html.dart:1:8: Error: Not found: 'dart:html'
    import 'dart:html';

Flutter Plugins found an alternative way to fix this by isolating clearly each platform into different libraries, which you can read more about in the official documentation.

But, there are times where we just want to quickly prototype a specific native interaction inside the same project, how can we do it?

Conditional Importing

The app that we are developing is a multi-platform application, available for Web and Mobile that has 1 fundamental difference upon login:

  1. On the Web, we are redirected to a new web page with a login page;
  2. On Mobile we use an existing library to log in.

As a principle, let’s assume we are not going to use any external libraries or plugins (such as url_launcher) to achieve or objective.

If we decide to solve the problem of redirecting to a new web page in Flutter Web we can follow the logic:

  1. Flutter Web has access to dart:html
  2. Dart on web can communicate with native JS functions
  3. With JS we can redirect to a new webpage with window.location.replace, as seen in w3school

Since window is a dart:html dependency, we cannot use it in Flutter Mobile, so our code will have to be isolated somehow.

Fortunately, Dart allows us to use Conditional Importing to import specific files per platform.

In essence, Dart checks if it can use dart:io or dart:html and imports the file that we have declared for it.

The example bellow will:

  1. Import src/hw_none.dart for a platform that hasn’t access to dart:io or dart:html, which essentially means that it’s a stub implementation that will not be used;
  2. If Dart has access to dart:io it will import src/hw_io.dart;
  3. Or, if it has access to dart:html it will import src/hw_html.dart.
import 'src/hw_none.dart' // Stub implementation
    if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
    if (dart.library.html) 'src/hw_html.dart'; // dart:html implementation

Let’s use this for our example of starting the login procedure.

The first thing that we should do is declare an abstract class that serves as the interface for both io and html:

abstract class BaseLogin {
  void login();
}

Since we are going to create different files, one thing we must be sure is that all implementation files have the same class name, so that they can be instantiated and called when imported.

We proceed to create a stub implementation for it in login/platform_impl/stub_login.dart:

import 'package:awesome_app/login/base_login.dart';

class LoginImpl extends BaseLogin {
  @overrideawesome_app
  void login() {
    throw Exception("Stub implementation");
  }
}

Next, we use this class to create our Web implementation in the login/platform_impl/web_login.dart file:

import 'dart:html';

import 'package:awesome_app/login/base_login.dart';

class LoginImpl extends BaseLogin {
  @override
  void login() {
    window.location.replace('https://strange-login-system.com/login');
  }
}

The mobile implementation is going to use a specific library, as stipulated before, in the login/platform_impl/mobile_login.dart file:

import 'package:awesome_app/login/base_login.dart';
import 'package:awesome_app/mobile_login/mobile_lib_for_login.dart';

class LoginImpl extends BaseLogin {
  final MobileLibForLogin _loginLib;

  LoginImpl() : _loginLib = MobileLibForLogin();

  @override
  void login() {
    _loginLib.loginMobile();
  }
}

Now there’s one missing piece for this puzzle - a file that will import conditionally all the files, and provide one single entry point for the login function - the login/login.dart file:

import 'platform_impl/stub_login.dart'
    if (dart.library.io) 'platform_impl/mobile_login.dart'
    if (dart.library.html) 'platform_impl/html_login.dart';


class Login {
  final LoginImpl _login;

  Login() : _login = LoginImpl();

  void login() {
    _login.login();
  }
}

In the end, we will have a folder structure such as:

.
├── login.dart
└── platform_impl
    ├── base_login.dart
    ├── mobile_login.dart
    ├── stub_login.dart
    └── web_login.dart

And now we can finally use it inside our application!

import 'package:awesome_app/login/login.dart';

class MyHomePage extends StatelessWidget {
  MyHomePage({Key? key}) : super(key: key);

  final login = Login();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: MaterialButton(
          color: Theme.of(context).primaryColor,
          child: const Text("Login"),
          onPressed: () {
            login.login();
          },
        ),
      ),
    );
  }
}

If we test it out, we see that not only does it compile for all platforms, but that we don’t have any errors when clicking the “Login” button 🙌

Conclusion

In this article we explored a couple of things:

With all of this, we can now create unique multi-platform apps with very specific use-cases such as a specific login system or using platform-specific APIs such as showing Alerts in Web or using the accelerometer in mobile.

I’m curious!

Have you been in a case where you had to create Multiplatform code? How did you do it?

Share it with me on Twitter 👉 @GonPalma, and if you liked the article, please consider following me there 💙

For more articles, be sure to check the rest of the blog 👇 https://gpalma.pt/blog/

Want to get the latest articles and news? Subscribe to the newsletter here 👇

Diving Into Flutter

In Diving into Flutter we will discover the Flutter Framework, bit by bit.

From the BuildContext, to pubspec files, going through exotic backend solutions, there won't be a stone left unturned.

Each week I'll also share one or two links for interesting resources I found around the Web

And for other articles, check the rest of the blog! Blog - Gonçalo Palma

Want to get the latest articles and news? Subscribe to Diving Into Flutter

Hey! I'm Gonçalo Palma! 👋

I often share Deep Diving articles on Flutter 💙