flutter_map_vector_tiles_mbtiles 1.0.0
flutter_map_vector_tiles_mbtiles: ^1.0.0 copied to clipboard
MBTiles support for flutter_map_vector_tiles: render a local .mbtiles archive as a flutter_map vector or raster source, fully offline, with SQLite reads kept off the UI isolate.
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_vector_tiles/flutter_map_vector_tiles.dart' as vt;
import 'package:flutter_map_vector_tiles_mbtiles/flutter_map_vector_tiles_mbtiles.dart';
import 'package:latlong2/latlong.dart';
/// Renders a local `.mbtiles` archive with a hosted style's theme.
///
/// ```
/// flutter run \
/// --dart-define=MBTILES_PATH=/absolute/path/to/region.mbtiles \
/// --dart-define=SOURCE_ID=openmaptiles \
/// --dart-define=STYLE_URL=https://tiles.openfreemap.org/styles/liberty
/// ```
///
/// `SOURCE_ID` is the id of the vector source in that style that the
/// archive should replace. If you do not know it, run once and read the
/// console: every source id the style declares is logged as it is
/// offered to [vt.StyleReader.resolveProvider].
///
/// Native only — there is no web target here, since MBTiles is SQLite
/// read through `dart:ffi`.
const _archivePath = String.fromEnvironment('MBTILES_PATH');
const _sourceId = String.fromEnvironment(
'SOURCE_ID',
defaultValue: 'openmaptiles',
);
const _styleUrl = String.fromEnvironment(
'STYLE_URL',
defaultValue: 'https://tiles.openfreemap.org/styles/liberty',
);
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(
title: 'flutter_map_vector_tiles_mbtiles example',
home: MbTilesMapPage(),
);
}
class MbTilesMapPage extends StatefulWidget {
const MbTilesMapPage({super.key});
@override
State<MbTilesMapPage> createState() => _MbTilesMapPageState();
}
class _MbTilesMapPageState extends State<MbTilesMapPage> {
late final Future<vt.Style> _style;
MbTilesVectorTileProvider? _archive;
@override
void initState() {
super.initState();
_style = _load();
}
Future<vt.Style> _load() async {
if (_archivePath.isEmpty) {
throw ArgumentError(
'Pass --dart-define=MBTILES_PATH=/path/to/region.mbtiles',
);
}
final archive = await MbTilesVectorTileProvider.open(
_archivePath,
logger: const vt.Logger.console(),
);
_archive = archive;
// The style contributes the theme, sprites and attribution; the
// archive contributes the tiles. Nothing about the tile path touches
// the network once the style is cached.
return vt.StyleReader(
uri: _styleUrl,
logger: const vt.Logger.console(),
resolveProvider: (id) async {
debugPrint('style declares source "$id"');
return id == _sourceId ? archive : null;
},
).read();
}
@override
void dispose() {
// Style.dispose() owns whatever resolveProvider handed back, so the
// archive is closed with it — disposing _archive here as well would
// be a double dispose.
_style.then((style) => style.dispose()).ignore();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const _MbTilesAppBar(),
body: FutureBuilder(
future: _style,
builder: (context, snapshot) {
final style = snapshot.data;
if (style == null) {
final error = snapshot.error;
return Center(
child: error != null
? Padding(
padding: const EdgeInsets.all(24),
child: Text('Failed to load:\n$error'),
)
: const CircularProgressIndicator(),
);
}
final bounds = _archive?.metadata.bounds;
return FlutterMap(
options: MapOptions(
// An archive covers one region, so start inside it rather
// than wherever the style suggests.
initialCenter:
bounds?.center ??
style.center ??
const LatLng(48.137, 11.575),
initialZoom: style.zoom ?? 12,
minZoom: 2,
maxZoom: 21,
),
children: [
vt.VectorTileLayer(
theme: style.theme,
tileProviders: style.providers,
rasterSources: style.rasterSources,
sprites: style.sprites,
logger: const vt.Logger.console(),
),
_Attribution(style: style, archive: _archive),
],
);
},
),
);
}
}
class _MbTilesAppBar extends StatelessWidget implements PreferredSizeWidget {
const _MbTilesAppBar();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) => AppBar(title: const Text('MBTiles'));
}
/// Whatever the style's sources declare, plus the archive's own
/// `attribution` row — archives routinely carry the one their data
/// requires, and it is not shown for you.
class _Attribution extends StatelessWidget {
final vt.Style style;
final MbTilesVectorTileProvider? archive;
const _Attribution({required this.style, required this.archive});
@override
Widget build(BuildContext context) {
final parts = <String>[
for (final attribution in style.attributions) attribution.text,
?archive?.metadata.attribution,
];
if (parts.isEmpty) return const SizedBox.shrink();
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(12),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Text(
'flutter_map | © ${parts.join(' · ')}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, color: Colors.black87),
),
),
),
),
);
}
}