Mastering Offline Maps in Flutter: A Deep Dive (Part 1)

Lavkant Kachhwaha
Captainfresh Tech
Published in
5 min readJan 7, 2024

In the vast landscape of mobile app development, incorporating robust offline map functionality is often a crucial requirement. Flutter, with its versatility, provides an excellent platform for achieving this, and in this series, we embark on a journey to master the art of implementing offline maps.

Introduction: Navigating the Offline World with Flutter

Mobile applications are no longer confined to areas with a stable internet connection. Users now expect seamless experiences even in remote locations or areas with poor network connectivity. Flutter, Google’s UI toolkit for building natively compiled applications, empowers developers to create cross-platform apps with a focus on a beautiful user interface and optimal performance.

In this series, we’ll explore the implementation of offline maps in Flutter, leveraging the power of Mapbox. Our journey begins with setting up the project and initializing the Mapbox plugin.

Setting the Stage: Flutter and Mapbox Integration

dependencies:
mapbox_gl: ^0.12.0

Now, you can use the following example to create a simple Flutter app with Mapbox:

import 'package:flutter/material.dart';
import 'package:mapbox_gl/mapbox_gl.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MapboxScreen(),
);
}
}

class MapboxScreen extends StatefulWidget {
@override
_MapboxScreenState createState() => _MapboxScreenState();
}

class _MapboxScreenState extends State<MapboxScreen> {
MapboxMapController? _controller;

void _onMapCreated(MapboxMapController controller) {
_controller = controller;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mapbox Flutter Example'),
),
body: MapboxMap(
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN', // Replace with your Mapbox access token
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: LatLng(37.7749, -122.4194), // San Francisco coordinates
zoom: 12.0,
),
),
);
}
}

Make sure to replace 'YOUR_MAPBOX_ACCESS_TOKEN' with your actual Mapbox access token. You can obtain an access token by signing up on the Mapbox website.

This example creates a Flutter app with a single screen (MapboxScreen) containing a Mapbox map. The MapboxMap widget is used with an onMapCreated callback to get a reference to the map controller. The initial camera position is set to San Francisco, but you can adjust it to any location.

Remember to run flutter pub get in your terminal to fetch the Mapbox plugin after updating your pubspec.yaml file. Additionally, ensure you have the necessary permissions to access the internet if you're running the app on a device or emulator.

What’s Next: Unraveling Advanced Map Features

dependencies:
flutter:
sdk: flutter
mapbox_gl: ^0.12.0
path_provider: ^2.0.0
rxdart: ^0.27.7
get_it: ^7.6.0
collection: ^1.0.0
void main() async {
GetIt getIt = GetIt.instance;

loadAllDependency() async {
getIt.registerSingleton<OfflineMapBloc>(OfflineMapBloc(), signalsReady: true);
}

await loadAllDependency();

runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const OfflineMapInit(),
);
}
}

Here’s a snapshot of essential code snippets from the OfflineMapBloc class that plays a pivotal role in this integration.

// Setting up Mapbox controller and initializing map features
await GetIt.instance<OfflineMapBloc>().initHybridComposition();
await GetIt.instance<OfflineMapBloc>().getAllOfflineRegionForToken();
await GetIt.instance<OfflineMapBloc>().getUserSelectedRegionAndDownloadRegionOffline();
  • Map Controller Handling: we initialize and manage the Mapbox controller, a crucial element for map interactions.
// Adding custom layers for bathymetry and PFZ (Precautionary Fishing Zone)
await GetIt.instance<OfflineMapBloc>().addCustomLayerBethemetry(controller!);
await GetIt.instance<OfflineMapBloc>().addCustomLayerPFZ(controller!);
  • Layer Customization: Explore the code that dynamically adds layers for bathymetry and PFZ, enhancing the map’s visual richness.
// Implementing user-interaction features like toggling boundaries and bathymetry
await GetIt.instance<OfflineMapBloc>().toggleInternationalBoundry();
await GetIt.instance<OfflineMapBloc>().toggleBathymetry();
  • User Controls: This enables the implementation of interactive controls for users, such as toggling international boundaries and bathymetry.
// Managing offline regions - downloading, checking, and deleting
await GetIt.instance<OfflineMapBloc>().getUserSelectedRegionAndDownloadRegionOffline();
await GetIt.instance<OfflineMapBloc>().isCurrentRegionDownloaded();
await GetIt.instance<OfflineMapBloc>().deleteUserDownloadedOfflineRegion();
  • Offline Region Handling: Managing offline regions, including downloading, checking if a region is already downloaded, and deleting downloaded regions.

Initialisation Workflow

class OfflineMapInit extends StatefulWidget {
const OfflineMapInit({super.key});

@override
State<OfflineMapInit> createState() => _OfflineMapInitState();
}

class _OfflineMapInitState extends State<OfflineMapInit> {
@override
void initState() {
initilizer();
super.initState();
}

void initilizer() async {
await GetIt.instance<OfflineMapBloc>().initHybridComposition();
await GetIt.instance<OfflineMapBloc>().getAllOfflineRegionForToken();
await GetIt.instance<OfflineMapBloc>().getUserSelectedRegionAndDownloadRegionOffline();

// Check if map is downloaded and redirect to map screen
ifMapDownloadedMoveToMapScreen();
}

void ifMapDownloadedMoveToMapScreen() async {
final isDownloaded = await GetIt.instance<OfflineMapBloc>().isCurrentRegionDownloaded();

if (isDownloaded == true) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => OfflineMapScreen(
item: GetIt.instance<OfflineMapBloc>().userSelectedRegionToDownload!,
)));
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => OfflineMapScreen(
item: GetIt.instance<OfflineMapBloc>().userSelectedRegionToDownload!,
)));
},
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
}

Let’s focus on the key elements and functionalities of the OfflineMapScreen widget, providing insights into how it facilitates offline map exploration in Flutter.

MapboxMap(
// Map configuration properties
onMapCreated: (controller) {
GetIt.instance<OfflineMapBloc>().setAndGetMapBoxController(newController: controller);
},
// Additional properties like accessToken, initialCameraPosition, styleString, etc.
// ...
onStyleLoadedCallback: GetIt.instance<OfflineMapBloc>().onStyleLoaded,
),

The MapboxMap widget is configured with essential properties for displaying the offline map. It also interacts with the OfflineMapBloc for specific functionalities.

Positioned(
bottom: 50,
left: 10,
child: Row(
children: [
// Zoom In
CircleAvatar(
child: IconButton(
onPressed: () {
GetIt.instance<OfflineMapBloc>().zoomIn();
},
icon: const Icon(Icons.add),
),
),
const SizedBox(width: 10),
// Zoom Out
CircleAvatar(
child: IconButton(
onPressed: () {
GetIt.instance<OfflineMapBloc>().zoomOut();
},
hoverColor: Colors.red,
icon: const Icon(Icons.remove),
),
),
],
),
),

The widget includes a user-friendly interface for zooming in and out, enhancing the overall map exploration experience.

Positioned(
bottom: 0,
left: 10,
child: Row(
children: [
// Toggle International Boundary
ElevatedButton(
onPressed: () {
GetIt.instance<OfflineMapBloc>().toggleInternationalBoundary();
},
child: const Text("Intl Boundary"),
),
const SizedBox(width: 10),
// Toggle PFZ (Preservation Fishing Zone)
ElevatedButton(
onPressed: () {
GetIt.instance<OfflineMapBloc>().togglePFZ();
},
child: const Text("PFZ"),
),
const SizedBox(width: 10),
// Toggle Bathymetry
ElevatedButton(
onPressed: () {
GetIt.instance<OfflineMapBloc>().toggleBathymetry();
},
child: const Text("BATH"),
),
],
),
),

Additional control buttons are provided for toggling features like International Boundary, Potential Fishing Zone (PFZ), and Bathymetry.

Extra : Use this Layer properties for rendering HeatMap on Mapbox
https://gist.github.com/Lavkushwaha/fe5dc630ca2c07d40b916fe18e7d5b6f

Conclusion: Mastering Offline Maps Part-1 — A Series Worth Following

In this blog post, we’ve delved into the exciting realm of offline maps within Flutter, employing the powerful Mapbox plugin to bring spatial intelligence to your applications. From the fundamental setup to the implementation of critical features, we’ve walked through key aspects to equip you with the skills needed for crafting location-aware Flutter apps.

Stay tuned for the upcoming posts in this series, where we’ll continue to unravel the potential of offline maps within Flutter. Your feedback and questions are invaluable, so feel free to share your thoughts.
The code snippets provided are just the beginning; there’s a lot more to explore and implement in the upcoming parts of this engaging series.

Stay curious, stay coding!

--

--