Flutter is Google’s new portable UI toolkit for building mobile, web, and desktop applications from a single codebase. Flutter makes it easy to build simple applications, however since it is relatively new if you need something beyond the most popular features you may find that you will need to roll your own solution, for instance offline maps.

With Google Maps support only recently being available, and no support for Apple Maps we need to find a third party option. Fortunately there is a MapBox Flutter plugin that will work for us https://github.com/tobrun/flutter-mapbox-gl. MapBox is a full featured mapping SDK with a rich feature set and tooling — including support for offline maps.

Create a new Flutter application

Install Flutter by following the instructions and create an application to verify that your development environment and workspace are setup properly before continuing.

$ flutter create map_app

$ cd map_app

$ flutter run

Register for a MapBox API token

Next we need to register for a MapBox API token. When you sign up you will get a default token which will work for our purposes. The API token is how MapBox tracks your usage and bills you, remember to review the API token settings before launching your application.

Add MapBox to your Flutter application

Add mapbox_gl to your pubspec.yaml file under the dependencies section. In order to get the latest code we will use the source instead of the dart package.



$ git clone $ cd map_app$ git clone https://github.com/tobrun/flutter-mapbox-gl.git

Then update your pubspec.yaml to reference the directory where the project was cloned.

dependencies:

...

mapbox_gl:

path: ./flutter-mapbox-gl

Install the mapbox_gl package and run the package

$ flutter pub get

Android Changes

Add your API token to the AndroidManifest

android/app/src/main/AndroidManifest.xml

<application

android:name="io.flutter.app.FlutterApplication"

android:label="map_app"

android:icon="@mipmap/ic_launcher"> <meta-data android:name="com.mapbox.token"

android:value="YOUR API KEY" /> <activity

...

</application>

You will also need to migrate the ./android directory to use androidx by adding the following two lines to ./android/gradle.properties

android.useAndroidX=true

android.enableJetifier=true

iOS Changes

ios/Runner/Info.plist

<plist version="1.0">

<dict>

...

<key>MGLMapboxAccessToken</key>

<string>YOUR API KEY</string

<key>io.flutter.embedded_views_preview</key>

<true/>

</dict>

</plist>

ios/Podfile

Uncomment the platform line to target iOS 9 required by the mapbox_gl plugin and add the use frameworks! line.

# Uncomment this line to define a global platform for your project

platform :ios, '9.0'

use_frameworks!

The Code

Test Run

We should now be able to run our application and see our map.

flutter run

Downloading Map data at build time.

To support offline maps we need to download the required Map tiles at build time and include them in our app. Mapbox has instructions on how to do this here — but the tl;dr version is below.

Build MapBox tools

$ git clone https://github.com/mapbox/mapbox-gl-native.git

$ cd mapbox-gl-native

$ make

Downloading the map tiles

In order to download all the required map tiles you will need the bounding box of the latitude/longitude you wish to cover as well as the supported zoom levels.

The style argument can be used to specify a custom style you have built using the Mapbox designer but we will use the default here.



--north 40.8365 \

--west -119.267 \

--south 40.7413 \

--east -119.1465 \

--minZoom 12 \

--maxZoom 18 \

--style

--token 'OBTAINED FROM YOUR MAPBOX.COM ACCOUNT' \

--output mapcache.db $ ./build/macos/Debug/mbgl-offline \--north 40.8365 \--west -119.267 \--south 40.7413 \--east -119.1465 \--minZoom 12 \--maxZoom 18 \--style mapbox://styles/mapbox/streets-v11 --token 'OBTAINED FROM YOUR MAPBOX.COM ACCOUNT' \--output mapcache.db

This command will download all the tiles required to render the map between the north/south/east/west coordinates given at the zoom levels 12–18.

If the user scrolls outside of that coordinates or the zoom level the MapBox API will attempt to download the tiles at runtime. So it is important to restrict the users ability to scroll and zoom if you want truly offline maps.

Adding the tiles

With the map tiles downloaded we add them to our assets directory. The db files needs to be named cache.db.

$ mkdir -p map_app/assets/

$ cp mapbox-gl-native/mapcache.db map_app/assets/cache.db

Update your pubspec.yaml to include the cache.db file into your app

assets:

- assets/cache.db

Copy tiles into place

In order for the mapview to use the cache.db we have to copy into the expected location. Furthermore to prevent the mapview from accessing the network completely we need to make sure the cache.db has been copied to the expected location before the mapview is ever loaded. This usually means we copy the cache.db into place during a “loading” screen.

import 'dart:async';

import 'dart:io';

import 'package:path/path.dart'; ... class _MapWidgetState extends State<MapWidget> {

...

var _tilesLoaded = false;

... @override

initState() {

super.initState();

_copyTilesIntoPlace();

} _copyTilesIntoPlace() async {

try {

await installOfflineMapTiles(join("assets", "cache.db"));

} catch (err) {

print(err);

} setState(() {

this._tilesLoaded = true;

});

} ... @override

Widget build(BuildContext context) {

if (this._tilesLoaded) {

return Container(

child: _buildMapBox(context),

);

} else {

return Center(

child: new CircularProgressIndicator(),

);

}

}

Run in Airplane mode

If we have done everything correctly, when we run the application in Airplane mode we can see that the map still loads.

Conclusion

With a little bit of research and the MapBox SDK we can now include offline map support in our applications for both iOS and Android.

Happy Fluttering!