local_mbtiles_server 0.1.0 copy "local_mbtiles_server: ^0.1.0" to clipboard
local_mbtiles_server: ^0.1.0 copied to clipboard

Serve SQLCipher-encrypted and plain MBTiles over localhost HTTP so any map SDK (MapLibre, Mapbox, etc.) can load offline tiles via standard URL templates.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:local_mbtiles_server/local_mbtiles_server.dart';

import 'map_screen.dart';
import 'map_session.dart';
import 'sqlcipher_password.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const LocalMbtilesServerExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'local_mbtiles_server example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ExampleHomePage(),
    );
  }
}

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

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  LocalMbtilesServer? _server;

  bool _loading = false;
  String? _error;
  String? _baseUrl;
  Map<String, dynamic>? _health;
  String? _sourceId;
  Map<String, dynamic>? _metadata;
  MapSession? _mapSession;

  Future<void> _fetchDemo() async {
    setState(() {
      _loading = true;
      _error = null;
      _baseUrl = null;
      _health = null;
      _sourceId = null;
      _metadata = null;
      _mapSession = null;
    });

    await _tearDown();

    try {
      final path = await _copySampleToTemp(demoMbtilesAsset);
      final source = await MbtilesSource.open(
        path: path,
        id: demoMbtilesSourceId,
        password: demoSqlCipherPassword,
      );
      final server = LocalMbtilesServer(config: MbtilesServerConfig(port: 0));

      await server.register(source);
      await server.start();

      final baseUrl = server.baseUrl;
      final health = server.health.toJson();
      final mbtilesMeta = await server.getMetadata(demoMbtilesSourceId);
      final mapSession = MapSession.fromMetadata(
        baseUrl: baseUrl,
        sourceId: demoMbtilesSourceId,
        metadata: mbtilesMeta,
      );
      final metadata = mbtilesMeta.toJson();

      if (!mounted) {
        await server.stop();
        return;
      }

      setState(() {
        _server = server;
        _baseUrl = baseUrl;
        _health = health;
        _sourceId = demoMbtilesSourceId;
        _metadata = metadata;
        _mapSession = mapSession;
        _loading = false;
      });
    } catch (error) {
      await _tearDown();
      if (!mounted) {
        return;
      }
      setState(() {
        _error = '$error';
        _loading = false;
      });
    }
  }

  Future<void> _tearDown() async {
    final server = _server;
    _server = null;

    if (server != null) {
      await server.stop();
    }
  }

  Future<String> _copySampleToTemp(String assetPath) async {
    final bytes = await rootBundle.load(assetPath);
    final directory = await Directory.systemTemp.createTemp(
      'local_mbtiles_server_',
    );
    final fileName = assetPath.split('/').last;
    final file = File('${directory.path}/$fileName');
    await file.writeAsBytes(
      bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),
    );
    return file.path;
  }

  @override
  void dispose() {
    _tearDown();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final canOpenMap = _mapSession != null && _sourceId != null && !_loading;

    return Scaffold(
      appBar: AppBar(title: const Text('local_mbtiles_server')),
      floatingActionButton: canOpenMap
          ? FloatingActionButton.extended(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute<void>(
                    builder: (context) => MapScreen(session: _mapSession!),
                  ),
                );
              },
              icon: const Icon(Icons.map_outlined),
              label: const Text('Show tile on map'),
            )
          : null,
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: ListView(
          children: [
            Text(
              'Example demo',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 16),
            const _SampleMbtilesSection(),
            const SizedBox(height: 16),
            FilledButton(
              onPressed: _loading ? null : _fetchDemo,
              child: Text(_loading ? 'Fetching...' : 'Fetch status & metadata'),
            ),
            const SizedBox(height: 24),
            if (_loading)
              const Center(child: CircularProgressIndicator())
            else if (_error != null)
              SelectableText(
                'Error:\n$_error',
                style: TextStyle(color: Theme.of(context).colorScheme.error),
              )
            else if (_health != null) ...[
              _ResultsSection(
                baseUrl: _baseUrl!,
                health: _health!,
                sourceId: _sourceId!,
                metadata: _metadata!,
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _SampleMbtilesSection extends StatelessWidget {
  const _SampleMbtilesSection();

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Sample MBTiles', style: theme.textTheme.titleMedium),
            const SizedBox(height: 8),
            Text(
              'Bundled synthetic raster overlay over San Francisco '
              '(zooms 12–15). Zoom out to see the base map; zoom in to see '
              'the colored localhost tiles.',
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 16),
            const _InfoRow(label: 'asset', value: demoMbtilesAsset),
            const _InfoRow(label: 'sourceId', value: demoMbtilesSourceId),
            const _InfoRow(label: 'format', value: 'png overlay (z12–15)'),
            const _InfoRow(label: 'encryption', value: 'SQLCipher passphrase'),
            const Divider(height: 24),
            Text(
              'SQLCipher password (passed to open)',
              style: theme.textTheme.labelLarge,
            ),
            const SizedBox(height: 4),
            SelectableText(
              demoSqlCipherPassword,
              style: theme.textTheme.bodySmall?.copyWith(
                fontFamily: 'monospace',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _ResultsSection extends StatelessWidget {
  const _ResultsSection({
    required this.baseUrl,
    required this.health,
    required this.sourceId,
    required this.metadata,
  });

  final String baseUrl;
  final Map<String, dynamic> health;
  final String sourceId;
  final Map<String, dynamic> metadata;

  @override
  Widget build(BuildContext context) {
    final encoder = const JsonEncoder.withIndent('  ');

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Server running', style: Theme.of(context).textTheme.titleMedium),
        const SizedBox(height: 8),
        _InfoRow(label: 'baseUrl', value: baseUrl),
        const SizedBox(height: 24),
        Text('server.health', style: Theme.of(context).textTheme.titleMedium),
        const SizedBox(height: 8),
        SelectableText(encoder.convert(health)),
        const SizedBox(height: 24),
        Text(
          'server.getMetadata($sourceId)',
          style: Theme.of(context).textTheme.titleMedium,
        ),
        const SizedBox(height: 8),
        SelectableText(encoder.convert(metadata)),
        const SizedBox(height: 50),
      ],
    );
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: RichText(
        text: TextSpan(
          style: DefaultTextStyle.of(context).style,
          children: [
            TextSpan(
              text: '$label: ',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            TextSpan(text: value),
          ],
        ),
      ),
    );
  }
}
2
likes
160
points
86
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Serve SQLCipher-encrypted and plain MBTiles over localhost HTTP so any map SDK (MapLibre, Mapbox, etc.) can load offline tiles via standard URL templates.

Repository (GitHub)
View/report issues

Topics

#mbtiles #maps #offline #sqlcipher

License

BSD-3-Clause (license)

Dependencies

flutter, meta, shelf, sqflite_sqlcipher

More

Packages that depend on local_mbtiles_server