Flutter: Conditional Import for Web and Native
The Rivepod way to provide platform specific implementations of a midi player
This article shows you how to use different implementations for web and for native. This is tricky when the web implementation needs to import a library which is not available for native platforms. A little example uses this trick to play midi on web and on native plaforms.
I’m working on an iOS app that will play midi sounds to learn the notes on a guitar. Most of the development I do on a really old MacBook Pro (2011) which can’t build iOS Apps any more. So I build my App for Web and this works fine. Only for deployment on my iPhone I use a newer Mac.
lib/midi_player_native.dart:5:8: Error: Not found: 'dart:js'
import 'dart:js' as js;
In an old school language like C this would be solved by the preprocessor and a conditional import of the correct source. In Dart such a conditional import is not so easy. A simple if(isWeb) won’t solve this problem. It can’t wrap an import and even if it could that wouldn’t help because it is evaluated only at runtime. So it it too late to avoid the search for the missing package.
Searching here on medium.com provided me with an article on how to solve my problem: ‘Conditional imports across Flutter and Web’ by Antonello Galipò. In his article he describes how to use different AuthManagers for web and native using the following Dart syntax:
He uses the singleton pattern and the trick is that the getManager() function is defined in three different files: in the stub, the web and the native file. If dart.library.io exists for the platform the getManager() in file auth0_manager.dart is used, otherwise the getManager() in file auth0_manager_for_web.dart returns a different implementation.
I adapted this code for my midi classes and it worked as expected. It’s a bit inconvenient to need four different files but this is because Dart alone needs three files for its syntax and an additional file contains the abstract class AuthManager or in my case MidiPlayer.
As I’m using the Riverpod package in all of my new projects, I thought I could use a similar construct without using a singleton and just taking the right function for the actual platform. I still need to spread the code over four files but now it fits nicely in my preferred provider structure:
The midiProvider returns just the right MidiPlayer class for the actual platform the app is build for.
The file midi_player_stub.dart is never really compiled by the Dart tools. It’s just a stub to make the Dart syntax checker happy.
For a native build the global function getMidiPlayer() from midi_player_native.dart returns an instance of class MidiPlayerNative. This class uses the flutter_midi package, which must load a sound file from the assets in the init() method.
It’s not nice to have the two global functions getMidiPlayer() but I don’t know a better solution. An alternative would be to give the two classes the same name, but I think this is more confusing.
If you want to know how the provider is used take a look at the following main.dart file:
You see how:
- runApp(ProviderScope(child: MyApp())); initilizes the riverpod state
- context.read(midiProvider).init(); uses the midiProvider to get the platform class and initializes it
- context.read(midiProvider).play(60); calls the play method of the platform class
If you are reading this articel not only because of the conditional import but also to use midi in your own web project then note that index.html must load the Tone.js library and provide the playNote() function, which is called in MidiPlayerWeb.
BTW: I’m using the logging package typically right from the start of a new project. So I can check whether my code is working as expected. Thanks to just a few lines of code, I see that the correct MidiPlayer class is used on both platforms. When I run the web version and I press the button “Press to play C-Major up” in the browser the console looks like this:
Restarted application in 175ms.
2021-01-01 17:56:25.423 INFO: MyApp: build loglevel: FINE
2021-01-01 17:56:25.425 FINE: MidiPlayerWeb: MidiPlayerWeb.init
2021-01-01 17:56:25.459 FINE: MyHomePage: build
2021-01-01 17:56:44.765 FINE: MyHomePage: _playMidi
2021-01-01 17:56:44.766 FINE: MidiPlayerWeb: play midi 60
2021-01-01 17:56:44.767 FINE: MidiPlayerWeb: playNote C4
2021-01-01 17:56:44.978 FINE: MidiPlayerWeb: play midi 64
2021-01-01 17:56:44.979 FINE: MidiPlayerWeb: playNote E4
2021-01-01 17:56:45.187 FINE: MidiPlayerWeb: play midi 67
2021-01-01 17:56:45.187 FINE: MidiPlayerWeb: playNote G4
On an iOS simulator it looks like this:
Xcode build done. 24,5s
Connecting to VM Service at ws://127.0.0.1:59209/iAX_ssPIKpc=/ws
flutter: 2021-01-01 17:51:25.901051 INFO: MyApp: build loglevel: FINE
flutter: 2021-01-01 17:51:25.934127 FINE: MidiPlayerNative: MidiPlayerNative.init
flutter: 2021-01-01 17:51:25.936137 FINE: MidiPlayerNative: Loading File... assets/Nylon Guitar.sf2
flutter: 2021-01-01 17:51:26.459887 FINE: MyHomePage: build
flutter: Result: Prepared Sound Font
flutter: 2021-01-01 17:51:33.826726 FINE: MyHomePage: _playMidi
flutter: 2021-01-01 17:51:33.827214 FINE: MidiPlayerNative: play midi 60
flutter: 2021-01-01 17:51:34.029797 FINE: MidiPlayerNative: play midi 64
flutter: 2021-01-01 17:51:34.236014 FINE: MidiPlayerNative: play midi 67
You find the full source on github: https://github.com/schilken/conditional_import
Many thanks to Rody Davis for his flutter_midi package and Antonello Galipò for his article about conditional imports.
If anyone knows a better way to handle such a multi platform problem, please let me know. Maybe the Dart language will provide a better way in the future– something like this?:
/* would be useful if this works in a future version of Dart :-)
const useWeb = true;