When working with a Flutter monorepo that implements both a web and mobile app, your try to maximize the amount of shared functionality or components. This makes your code more maintainable, reusable and easier to update.
However, some Flutter packages are platform-specific (for example, a package that works only on mobile but not on web). This creates a compilation issue when the common package tries to use an unsupported package on the wrong platform.
Example: Using a tile provider
Let’s say you have shared map widget using the flutter_map package for both you mobile as web app. At some point, you’ll start optimizing the map and look into the tileProvider parameter to optimize the map tiles.
For a web implementation, you might want to use CancellableNetworkTileProvider from the flutter_map_cancellable_tile_provider package. But for mobile, the FMTCStore from the flutter_map_tile_caching package is a better fit. Normally, you would just solve it with a ternary operator. Simple right?
import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
class MyMap extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlutterMap(
options: MapOptions(
center: LatLng(51.509364, -0.128928),
zoom: 9.2,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
tileProvider: kIsWeb
? CancellableNetworkTileProvider()
: FMTCTileProvider(store: FMTCStore.instance('mapStore')),
),
],
);
}
}
Sadly enough, this wont work. The tile caching package has no Flutter web support, which means we can’t use it in our shared widget. Even just importing, without actually using, it will result in build errors.
ℹ️ Packages without specific platform support won’t compile or can’t be imported for this platform. Even if you don’t actually use them (e.g., within an if statement).
This is where conditional imports step in. A technique that allows you to use different implementations based on the platform the app is running on.
Conditional imports
The high level file structure looks like this.
tile_provider/
tile_provider.dart # The main interface file
platform_impl/
base_tile_provider.dart # Abstract class that
stub_tile_provider.dart # Default implementation
mobile_tile_provider.dart # iOS/Android implementation
web_tile_provider.dart # Web implementation
🧱 Base tile provider
Defines the abstract interface or base class that all platform-specific implementations must follow. This ensures that:
- All implementations provide the same methods and functionality
- The rest of the application can rely on a consistent API regardless of platform
- Type safety is maintained across the codebase
import 'package:flutter_map/flutter_map.dart';
abstract class BaseTileProvider {
TileProvider getTileProvider();
}
🚧 Stub tile provider
A basic implementation that throws an error when used, serving as a safety net to ensure your code compiles even when platform-specific implementations are missing. It's like a placeholder that clearly indicates when a platform-specific version needs to be implemented.
import 'package:flutter_map/flutter_map.dart';
import 'base_tile_provider.dart';
class TileProviderImpl implements BaseTileProvider {
@override
TileProvider getTileProvider() {
throw UnimplementedError(
'Tile provider for this platform is not implemented');
}
}
🖥️ 📱 Web and mobile tile provider
The actual platform-specific implementations. These files can import the required packages, independent of their platform.
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart';
import 'base_tile_provider.dart';
class TileProviderImpl implements BaseTileProvider {
@override
TileProvider getTileProvider() {
return CancellableNetworkTileProvider();
}
}
ℹ️ All of the classes that implement this abstract TileProvider (stub, mobile and web) are called TileProviderImpl. This is to make sure whichever file you import, it’s always a TileProviderImpl.
🪄 Tile provider
This is where the magic happens. With a single import statement, it selects between the three different implementations; default (stub), mobile and web.
import 'package:flutter_map/flutter_map.dart';
import 'platform_impl/stub_tile_provider.dart'
if (dart.library.io) 'platform_impl/mobile_tile_provider.dart'
if (dart.library.html) 'platform_impl/web_tile_provider.dart';
class PlatformTileProvider {
PlatformTileProvider() : _tileProvider = TileProviderImpl();
final TileProviderImpl _tileProvider;
TileProvider getTileProvider() {
return _tileProvider.getTileProvider();
}
}
How it works
- The PlatformTileProvider class serves as a unified interface for all platforms
- At compile time, Dart selects the appropriate implementation based on the platform
- Each platform-specific file implements the same interface (TileProviderImpl)
- The rest of the app can use PlatformTileProvider without worrying about platform differences
This pattern is a powerful way to maintain a clean codebase while handling platform-specific functionality, allowing developers to write platform-agnostic code that works seamlessly across mobile and web.