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:
On the other hand, Riverpod, also by Remi, is available for all Flutter platforms plus Dart native and Dart for Web:
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:
But the camera plugin does:
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:
- On the Web, we are redirected to a new web page with a login page;
- 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:
- Flutter Web has access to dart:html
- Dart on web can communicate with native JS functions
- 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:
- Import
src/hw_none.dart
for a platform that hasn’t access todart:io
ordart:html
, which essentially means that it’s a stub implementation that will not be used; - If Dart has access to
dart:io
it will importsrc/hw_io.dart
; - Or, if it has access to
dart:html
it will importsrc/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:
- How to correctly pick libraries for our apps, depending on the platforms we are targeting;
- Why some packages or plugins don’t work right-out-the-box for all platforms;
- How to use conditional importing to use native APIs inside our applications without the need to create a separate plugin.
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 👇
And for other articles, check the rest of the blog! Blog - Gonçalo Palma