Only this pageAll pages
Powered by GitBook
1 of 21

v10

Loading...

Loading...

Loading...

Loading...

Get Started

Loading...

Loading...

Loading...

Loading...

Usage

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Is FMTC Right For Me?

In a one word answer: Yes.

FMTC aims to provide all the functionality you will need for advanced offline mapping in your app, and the unique features that will help set your app apart from the competition, in a way that requires little knowledge of the internals of 'flutter_map' and other caching fundamentals, and little effort from you (for most setups).

However, this doesn't mean there aren't any other options to consider! The flutter_map documentation gives a good overview of the different types of caching.

In general, there's a few reasons why I wouldn't necessarily recommend using FMTC:

  • You're not planning to make use of bulk downloading or import/export functionality

  • You don't need the fine grained control of stores (and their many-to-many relationship with tiles that keeps duplication minimal across them)

  • You want to ship every user a standardized tileset, and you don't really need other functionality after that point

Although FMTC will still handle all these situations comfortably, other options may be better suited, and/or more lightweight.

FMTC is an all-in-one solution. It specialises in the functionalities which are difficult and time-consuming to get right (such as bulk downloading), and their integration with the essential function of browse caching, through a clean, unified API, and a clean, decluttered, and fast backend.

Other libraries or DIY solutions may not be all-in-one, but they may be all that's required for your app. Caching alone is not difficult or time consuming to setup yourself, either through a custom TileProvider backed by a non-specialised image caching provider such as 'cached_network_image', or the other browse-caching-only flutter_map plugin 'flutter_map_cache' if you need to save even more time at the expense of more external dependencies.

Another consideration for non-GPL-licensed projects is abiding by FMTC's GPL license. This is especially true for proprietary products.

For more information about licensing, please see (Proprietary) Licensing.

If you're still not sure, please get in touch: ! I'm always happy to offer honest guidance :)

Example Application

This package contains a full example application - prebuilt for Android and Windows by GitHub Actions - showcasing the most important features of this package and its modules.

The example app isn't intended for beginners or as a starting point for a project. It is intended for evaluation purposes, to discover FMTC's capabilities, and how it might be implemented into an app.

To start using FMTC in your own app, please check out the Quickstart guide instead.

The example application pairs perfectly with the testing tile server included in the FMTC project: Testing Tile Server!

Prebuilt Artifacts

If you can't build from source for your platform, our GitHub Actions CI system compiles the example app to artifacts for Windows and Android, which just require unzipping and installing the .exe or .apk found inside.

Note that these artifacts are built automatically from the 'master' branch, so may not reflect the the latest release on pub.dev.

Build From Source

If you need to use the example app on another platform, you can build from source, using the 'example' directory of the repository.

Supporters

Supporting Me

Hi there 👋 My name is Luka, but I go by JaffaKetchup!

I'm currently in full-time education in the UK studying Computer Science, Geography, and Mathematics, and have been building my software development skills alongside my education for many years. I specialise in the Flutter and Dart technologies to build cross-platform applications, and have over 4 years of experience both from a hobby and commercial standpoint.

I've worked as a senior developer with a small team at WatchEnterprise to develop their Flutter-based WatchCrunch social media app for Android & iOS.

I'm one of a small team of maintainers for Flutter's №1 non-commercially aimed mapping library 'flutter_map', for which we make internal contributions, regulate and collaborate with external contributors, and offer support to a large community. I also personally develop a multitude of extension libraries, such as 'flutter_map_tile_caching'. In addition, I regularly contribute to OpenStreetMap, and continously improve my skills with personal experimental projects.

I'm eager to earn other languages, and more than happy to talk about anything software development related!

Sponsorships & donations allow me to further my education, whilst spening even more time developing the open-source projects that you use & love. I'm grateful for any amount you can spare, all support means a lot to me :) If you can't support me financially, please consider leaving a star and a like on projects that worked well for you.

I'm extremely greatful for any amount you can spare!

Supporters

Many thanks to all my supporters, donations of all sizes mean a lot to me, and encourage and enable me to continue my work on FMTC and other open-source projects.

In no particular order:

  • + more anonymous or private donors

And also a thank you to the businesses listed in , and more, for abiding by the GPL license of this repository.

@tonyshkurenko
@Mmisiek
@huulbaek
@andrewames
@ozzy1873
@eidolonFIRE
@weishuhn
@mohammedX6
@quentinchaignaud
@Mayb3Nots
@T-moz
@micheljung

(Proprietary) Licensing

FMTC is licensed under GPL-v3.

If you're developing an application that isn't licensed under GPL, this affects you and your application's legal right to distribution.

I am not a lawyer, and this information is to the best of my understanding only. You are urged to read the license yourself for a thorough understanding.

This project is released under GPL v3. For detailed information about this license, see https://www.gnu.org/licenses/gpl-3.0.en.html. choosealicense.com summarises the license with the following paragraph:

Permissions of this strong copyleft license are conditioned on making available complete source code of licensed works and modifications, which include larger works using a licensed work, under the same license. Copyright and license notices must be preserved. Contributors provide an express grant of patent rights.

Essentially, whilst you can use this code within commercial projects, they must not be proprietary - they incorporate this 'licensed work' so they must be available under the same license. You must distribute your source code (at least on request) (under the same GPL v3 license) to anyone who uses your program.

I learnt (and am still learning) to code with free, open-source software due to my age and lack of money, and for that reason, I believe in promoting open-source wherever possible to give equal opportunities to everybody, no matter their age, financial position, or any other characteristic. I'm not sure it's fair for commercial & proprietary applications to use software made by people for free out of generosity without giving back to the ecosystem or maintainer(s). On the other hand, I recognise that commercial businesses may want to use my projects for their own proprietary applications, and are happy to support me, and I am also trying to make a small amount of money from my projects, by donations and by selling licenses!

Therefore, if you would like a license to use this software within a proprietary application, I am willing to sell a (preferably yearly) license. If this seems like what you'd be interested in, please do not hesitate to get in touch at [email protected]. Please include details of your project if you can, and the approximate scale/audience for your app; I try to find something that works for everyone, and I'm happy to negotiate! If you're a non-profit organization, I'm happy to also offer an alternative license for free!

Want to see who else uses FMTC? Check out !

Installation

FMTC is licensed under GPL-v3.

If you're developing an application that isn't licensed under GPL, this affects you and your application's legal right to distribution. For more information, please see (Proprietary) Licensing.

Looking to start using FMTC in your project? Check out the Quickstart guide!

Install

For the latest stable release, depend on the package as you normally would by adding it to your pubspec.yaml manually or using:

To depend on potentially unstable commits from a branch (the commits on main usually represent stable releases), for development or testing, follow from pub.dev, then add the following lines to your pubspec.yaml file under the dependencies_override section:

Then, depending on the platforms you are developing for, you may need to follow ObjectBox's installation instructions for your platform (which can be found originally here, under the Flutter tab):

Try building the app - it might just work, especially if you are using other plugins in your app!

If it does not build successfully

If the error message seems to indicate that the "Android NDK" version needs to be higher, follow the instructions.

Usually this involves the following change to your app-level build.gradle(.kts) config:

macOS apps may need to target macOS 10.15. In your Podfile, change the platform and in the Runner.xcodeproj/project.pbxproj file, update MACOSX_DEPLOYMENT_TARGET.


To enable your app to run in sandboxed mode (which is a requirement for most applications), you'll need to specify an application group. Follow these instructions:

  1. Check all macos/Runner/*.entitlements files contain a section with the group ID

  2. If necessary, change the string value to the DEVELOPMENT_TEAM you can find in your Xcode settings, plus an application-specific suffix. Due to macOS restrictions the complete string must be 19 characters or shorter. For example:

  3. When initialising FMTC, make sure to pass this string to the macosApplicationGroup argument

Although FMTC will compile on the web, the default FMTC backend does not support the web platform. FMTCObjectBoxBackend.initialise and .uninitialise will throw UnsupportedErrors if invoked on the web. Other methods will throw RootUnavailable as normal.

Import

After installing the package, import it into the necessary files in your project:

import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';

Testing Tile Server

A miniature tile server, intended to test and calibrate FMTC, has been included in the project.

Avoid making too many costly and slow requests to your chosen tile server during development by using this miniature tile server!

For internal testing and development purposes, it also doubles down as a handy way to test your application without making too many costly and slow requests to your chosen tile server. When in use with the example application, it can handle over 2000 tiles/second.

It is a very simple web HTTP server written in Dart, that responds to all* requests with a tile. There is a theoretically 90% chance that this tile will be a specific land tile, and a 10% chance that it will be a sea tile - designed to test the sea tile skipping functionality. There are only these two tiles - it is not a full tile server.

the Land Tile 90% chance
the Sea Tile 10% chance

To use this tile server:

The tile server is hardcoded to use standard HTTP port 7070 to serve content, which is usually free. Other programs must not be using this port.

  1. Download/compile & start the tile server (no permanent installation required)

    • On Windows or Linux Download a copy of the latest '<platform>-ts' artifact from GitHub Actions, and run the executable inside: https://nightly.link/JaffaKetchup/flutter_map_tile_caching/workflows/main/main

    • On other platforms Clone the FMTC GitHub repository to your device, then run '/tile_server/bin/tile_server.dart' manually

  2. Use the following URL to connect to it

    • From the local device: http://localhost:7070/{z}/{x}/{y}.png

    • From the same network (on another device): http://<your-local-ip>:7070/{z}/{x}/{y}.png To find your local IP address, follow the instructions for your OS here

  3. Control the tile server using keyboard key presses in the console window

    • q: Release port 7070 and quit the executable

    • UP arrow: Increase the artificial delay between request and response by 2ms

    • DOWN arrow: Decrease the artificial delay between request and response by 2ms

v9 -> v10 Migration

v10 focuses on completing the 'tiles-across-stores' functionality from v9, by bringing it to browse caching, which huge amounts of customizability and flexibility.

Check out the CHANGELOG: ! This page only covers breaking changes, not feature additions and fixes.

Please consider donating: ! Any amount is hugely appreciated!

Browse Caching

We recommend following the steps on from start to finish to migrate your existing FMTCTileProvider to v10, as the API has significant changes, and the steps include new guidance to ensure best practice and performance.

Some changes are highlighted below:

Absorbed FMTCTileProviderSettings directly into FMTCTileProvider

The properties within have become properties directly in FMTCTileProvider. This also means the automatic global system (where the settings could be set once then used everywhere) has also been removed.

The simplifies the code internals and removes an unnecessary layer of abstraction.

Replaced maxStoreLength with property directly on stores

It has been replaced with a property on each store itself. It can be set at creation, or changed after, and read at any time:

This is more suitable for providers now that more than one store may be used, potentially each with a different maximum length.

Replaced obscuredQueryParams with urlTransformer

It has been replaced on the FMTCTileProvider with a more flexible custom callback which may perform any processing logic required, and a utility method if the old behaviour is still desired.

To migrate directly, see the example setup: .

Renamed CacheBehavior with BrowseLoadingStrategy

This has been renamed to fit better with a newly introduced enumerable that work together to configure the tile provider's logic.

(And also, no more US/UK confusion :D)

Bulk Downloading

Most of bulk downloading hasn't had any breaking changes, with the major exception of these:

startForeground now returns two streams

It now returns one stream of DownloadProgresss, and one of the new TileEvents. This means that checks no longer have to be made to ensure a TileEvent is not a repeated event (except where using the new feature to retry failed tiles, discussed below), and also means they can be more easily listened to independently.

To migrate, listen to necessary streams seperately.

Renovated TileEvent completely

Properties in v9 were nullable dependent on whether they were available, and this could be checked with .result (TileEventResult).

TileEvent has been split into a tree of classes, which are sealed. This means that the available properties are fully safe and no null-checks need to be made. Switch-case statements and normal is checks can be used, which statically changes the type of the TileEvent to a subtype appropriately. Each subtype represents a specific outcome of the tile download, and mixes in certain types.

  • SuccessfulTileEvent is emitted when a tile is successfully downloaded Root subtype, mixes in TileEventFetchResponse (makes the raw fetch response from the server available) and TileEventImage (makes tile image available)

  • SkippedTileEvent () Root subtype, mixes in TileEventImage

    • ExistingTileEvent is emitted when a tile is skipped because it already exists

    • SeaTileEvent is emitted when a tile is skipped because it was a sea tile Also mixes in TileEventFetchResponse

  • FailedTileEvent () Root subtype

    • NegativeResponseTileEvent is emitted when a tile fails because the server did not respond with 200 OK Mixes in TileEventFetchResponse

    • FailedRequestTileEvent is emitted when a tile fails because the request to the server was not made successfully (eligible for retry)

Renamed properties within DownloadProgress

Most are renamed obviously to improve clarity. Some may have had the exact included figures changed.

Quickstart

FMTC is licensed under GPL-v3.

If you're developing an application that isn't licensed under GPL, this affects you and your application's legal right to distribution. For more information, please see .

This page guides you through a simple, fast setup of FMTC that just enables basic browse caching, without any of the cool features that you can discover throughout the rest of this documentation.

1.

Depend on the latest version of the package from pub.dev, then import it into the appropriate files of your project.

Depending on your platform, some additional setup may be neccessary (particularly on macOS).

2.

Perform the startup procedure to allow usage of FMTC's APIs and allow FMTC to spin-up the underlying connections & systems.

Here, we'll use the built-in, default 'backend' storage, which uses . We'll perform the initialisation just before the app starts, so we can be sure that it will be ready and accessible throughout the app, at any time.

3.

Create a container that is capable of storing tiles, and can be used to and bulk download.

Here, we'll create one called 'mapStore', directly after initialisation. Any number of stores can be created, at any point!

4.

Add FMTC's specialised TileProvider to the TileLayer, to enable browse caching, and retrieval of tiles from the specified store.

Double check that the name of the store specified here is the same as the store created above!

5. Wow! Look at that caching...

You should now have a basic working implementation of FMTC that caches tiles for you as you browse the map!

There's a lot more to discover, from store management to bulk downloading, and from statistics to exporting/importing.

Before using FMTC, especially to bulk download or import/export, ensure you comply with the appropriate restrictions and terms of service set by your tile server. Failure to do so may lead to any punishment, at the tile server's discretion.

This library and/or the creator(s) are not responsible for any violations you make using this package.

For example, OpenStreetMap's tile server forbids bulk downloading: . And Mapbox has restrictions on importing/exporting from outside of the user's own device.

For testing purposes, check out the testing tile server included in the FMTC project: .

Root

A root contains statistics about itself and the stores, as well as information for the bulk download system, and access to the import/export functionality.

Roots are unnamed, and the current root is accessed through FMTCRoot:

To manage the root, use the methods on the backend.

Statistics

FMTCRoot.stats allows access to statistics, as well as listing of all existing stores, and the watching of changes in multiple/all stores.

Remember that the size and length statistics in the root may not be equal to the sum of the same statistics of all available stores, because tiles may belong to many stores, and these statistics do not count any tile multiple times.

Recovery

Import/Export

Exporting

The export() method copies the stores, along with all necessary tiles, to an archive at the specified location (creating it if non-existent, overwriting it otherwise), in the FMTC (.fmtc) format.

The specified stores must contain at least one tile.

Archives are backend specific. They cannot necessarily be imported by a backend different to the one that exported it.

Root & Stores

FMTC uses a root and stores to structure its data. In general, a single root exists (which uses a single ), which contains multiple named stores. Cached tiles can belong to multiple stores, which reduces duplication and maximizes flexibility.

The structures use the ambient backend when a method is invoked on it, not at construction time.

Therefore, it is possible to construct an FMTCStore/FMTCRoot before initialisation, but 'using' any methods on it will throw RootUnavailable.

Import/Export

FMTC allows stores (including all necessary tiles and metadata) to be exported to an 'archive'/a standalone file, then imported on the same or a different device!

Before using FMTC, especially to bulk download or import/export, ensure you comply with the appropriate restrictions and terms of service set by your tile server. Failure to do so may lead to any punishment, at the tile server's discretion.

This library and/or the creator(s) are not responsible for any violations you make using this package.

For example, OpenStreetMap's tile server forbids bulk downloading: . And Mapbox has restrictions on importing/exporting from outside of the user's own device.

For testing purposes, check out the testing tile server included in the FMTC project: .

FMTC does not support exporting tiles to a raw Z/X/Y directory structure with image files that can be read by other programs.

For example, this can be used to create backup systems to allow users to store maps for later off-device, sharing/distribution systems, or to distribute a preset package of tiles to all users without worrying about managing IO or managing assets, and still allowing users to update their cache afterward!


External functionality is accessed via FMTCRoot.external('~/path/to/file.fmtc').

The path should only point to a file. When used with export, the file does not have to exist. Otherwise, it should exist.

The path must be accessible to the application. For example, on Android devices, it should not be in external storage, unless the app has the appropriate (dangerous) permissions.

On mobile platforms (/those platforms which operate sandboxed storage), it is recommended to set this path to a path the application can definitely control (such as app support), using a path from 'package:path_provider', then share it somewhere else using the system flow (using 'package:share_plus').

Console/Terminal
flutter pub add flutter_map_tile_caching
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
main.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';

Future<void> main() async {
    WidgetsFlutterBinding.ensureInitialized();
    
    await FMTCObjectBoxBackend().initialise(...);
    
    // ...
    
    runApp(MyApp());
}
main.dart
    // ...
    
    await FMTCStore('mapStore').manage.create();
    
    // ...
map_view.dart
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';

// Stateful widget class definition

class _...State extends State<...> {
  final _tileProvider = FMTCTileProvider(
    stores: const {'mapStore': BrowseStoreStrategy.readUpdateCreate},
  );
  
  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      options: MapOptions(),
      children: [
        TileLayer(
          urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
          userAgentPackageName: 'com.example.app',
          tileProvider: _tileProvider,
          // Other parameters as normal
        ),
      ],
    );
  }
}
(Proprietary) Licensing
Install
Initialise
ObjectBox
Create a store
Connect to 'flutter_map'
https://operations.osmfoundation.org/policies/tiles
Testing Tile Server
backend
https://operations.osmfoundation.org/policies/tiles
Testing Tile Server
Trusted by many
// final root = FMTCRoot;
final databaseSize = await FMTCRoot.stats.realSize;
Recovery
Recovery
Import/Export
await FMTCRoot.external('~/path/to/file.fmtc').export(['storeName']);
https://pub.dev/packages/flutter_map_tile_caching/changelog
Integrating With A Map
Supporting Me

Stores

Stores maintain references to all tiles which belong to it, and also contain customizable metadata and cached statistics.

They are referenced by name, the single argument of FMTCStore.

Ensure names of stores are consistent across every access. "Typed"/code-generated stores are not provided, to maintain flexibility.

Construction of an FMTCStore object does create the underlying store, as this is an asynchronous task. It must be created before it may be used.

// final store = FMTCStore('storeName');
await FMTCStore('storeName').manage.create(); // Creates the store

Management

FMTCStore().manage allows control over the store and its contents.

Statistics

FMTCStore().stats allows access to:

  • statistics

  • retrieval of a recent tile (as an image)

  • watching of changes to the store

Metadata

FMTCStore().metadata allows access and control over a simple persistent storage mechanism, designed for use with custom data/properties/fields tied to the store. For example, in some apps, it could store the BrowseStoreStrategy or URL template/source.

Data is interpreted in key-value pair form, where both the key and value are Strings. Internally, the default backend stores it as a flat JSON structure. The metadata is stored directly on the store: if the store is deleted, it is deleted, and an exported store retains its metadata. More advanced requirements will require use of a separate persistence mechanism.

Remember that metadata does not have any effect on internal logic: it is simply an auxiliary method of storing any data that might need to be kept alongside a store.

Importing

The import() method copies the specified archive to a temporary location, then opens it and extracts the specified stores (or all stores if none are specified) & all necessary tiles, merging them into the in-use database. The specified archive must exist, must be valid, and should contain all the specified stores, if applicable.

There is no support for directly overwriting the in-use database with the archived database, but this may be performed manually while FMTC is uninitialised.

There must be enough storage space available on the device to duplicate the entire archive, and to potentially grow the in-use database.

This is done to preserve the original archive, as this operation writes to the temporary archive. The temporary archive is deleted after the import has completed.

final importResult =
    await FMTCRoot.external('~/path/to/file.fmtc').import(['storeName']);

The returned value is complex. See the API documentation for more details:

Conflict Resolution Strategies

If an importing store has the same name as an existing store, a conflict has occurred, because stores must have unique names. FMTC provides 4 resolution strategies:

  • skip Skips importing the store

  • replace Deletes the existing store, replacing it entirely with the importing store

  • rename Appends the current date and time to the name of the importing store, to make it unique

  • merge Merges the two stores' tiles and metadata together

In any case, a conflict between tiles will result in the newer (most recently modified) tile winning (it is assumed it is more up-to-date).

List Stores

If the user must be given a choice as to which stores to import (or it is helpful to know), and it is unknown what the stores within the archive are, the listStores getter will list the available store names without performing an import.

The same storage pitfalls as import exist. listStores must also duplicate the entire archive.

Recovery

RootRecovery, accessed via FMTCRoot.recovery, allows access to the bulk download recovery system, which is designed to allow rescue (salvation and restarting) of failed downloads when they crashed due to an unexpected event.

// List all recoverable regions, and whether each one has failed
await FMTCRoot.recovery.recoverableRegions; 
// List all failed downloads
await FMTCRoot.recovery.recoverableRegions.failedOnly; 
// Retrieve a specific recoverable region by ID
await FMTCRoot.recovery.getRecoverableRegion();
// Safely remove the specified recoverable region
await FMTCRoot.recovery.cancel(); 

RecoveredRegion

RecoveredRegions are wrappers containing recovery & some downloadable region information, around a DownloadableRegion.

Once a RecoveredRegion has been retreived, it contains the original BaseRegion in the region property.

To create a DownloadableRegion using the other available information with a provided TileLayer, use toDownloadable.

A RecoveredRegion (and a DownloadableRegion generated from it) will point to only any remaining, un-downloaded tiles from the failed download.

The start tile will be adjusted from the original to reflect the progress of the download before it failed, meaning that tiles already successfully cached (excluding buffered) will not be downloaded again, saving time and network transfers. The end tile will be either the original, or the maximum number of tiles normally in the region (which will have no resulting difference than null, but allows for a quick estimate of the number of remaining tiles to be made without needing to recheck the entire region).

flutter_map_tile_caching

A plugin for 'flutter_map' providing advanced offline functionality

Welcome to v10

If you're coming from v9, it might help to see the . Or, if you'd like to stay on v9 for the time being, that !

Highlights

Trusted by many

In addition to our generous , FMTC is also trusted and all around the world. Here's just a few!

How can FMTC elevate my app to the next level?

Easy! Take a look at Google Maps, or Strava, or whichever other app of your choice.

More than just preset rectangles

Whether it's walking along a remote winding river using the Line region, downloading all of central London ready for that weekend exploration using the Circle region (roaming fees + maps gets expensive fast!), or tracking your belongings across a vast, shapeless space using the Custom Polygon region, FMTC has your user's back - but not all of their storage space!

Keep storage usage efficient

With Sea Tile Skipping, you can avoid storing unnecessary tiles of pure sea, then use the map client's functionality to just paint the spaces the same color as the sea.

Highly flexible stores allow for multiple stores to be used to more efficiently store and control areas, and tiles can be used by multiple stores without duplication.

Import/export functionality allows users to temporarily offload stores to another device to keep their device lightweight until they require those tiles, without having to download them from the origin server again.

Raster tiles do consume more storage than vector tiles. Vector tile support is planned and already possible with some custom integration.

Highly controllable and flexible downloads

Bulk downloads can be paused and resumed at any time, and with download recovery, downloads that stopped unexpectedly can be started right from where they left off, without your user even knowing something went wrong.

I wonder how much it costs the app developers?

FMTC supports bulk downloading from any tile server, so you can choose whichever one suits you best.

Our browse caching mechanism doesn't result in any extra requests to the tile server, and in fact can reduce costs by serving tiles to users from their local cache without cost. Or, if you're running your own server, you can reduce the strain on it, keeping it snappy with fewer resources!

Downloads can be rate limited to avoid running up to the server's rate limit or excess fee.

And with export/import functionality, user's can download regions just once, then keep the download themselves for another time. Or, you can provide a bundle of tiles to all your user's, while still allow it to be updated per-user in future!

For (proprietary) licensing information, as FMTC is licensed under GPL-v3, please see .


Supporting Me

This project is wholly open source and funded by generous supporters like you! Any amount you can spare that you think FMTC deserves is hugely appreciated, and means a lot to me :)

(Proprietary) Licensing

FMTC is licensed under GPL-v3.

If you're developing an application that isn't licensed under GPL, this affects you and your application's legal right to distribution.

Get Help

Not quite sure about something? No problem, I'm happy to help!

Please get in touch via the correct method below for your issue, and I'll be there to help ASAP!

  • For bug reports & feature requests:

  • For implementation/general support: The #plugin channel on the

  • For other inquiries and licensing:

Get Help
Trusted by many

Initialisation

FMTC is licensed under GPL-v3.

If you're developing an application that isn't licensed under GPL, this affects you and your application's legal right to distribution. For more information, please see (Proprietary) Licensing.

FMTC relies on a self-contained 'environment', called a Backends, that requires initialisation (and configuration) before it can be used. This allows the backend to start any necessary seperate threads/isolates, load any prerequisites, and open and maintain a connection to a database. This environment/backend is then accessible internally through a() singleton, so initialisation is not required again.

Initialisation

Initialisation should be performed before any other FMTC or backend methods are used, and so it is usually placed just before runApp, in the main method. This shouldn't have any significant effect on application startup time.

If initialising in the main method before runApp is called, ensure you also call WidgetsFlutterBinding.ensureInitialised() prior to the backend initialisation.

main.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';

Future<void> main() async {
    WidgetsFlutterBinding.ensureInitialized();   
    
    try {
        await FMTCObjectBoxBackend().initialise(...); // The default/built-in backend
    } catch (error, stackTrace) {
        // See below for error/exception handling
    }
    
    // ...
    
    runApp(MyApp());
}

Do not call any other FMTC methods before initialisation. Doing so will cause a RootUnavailable error to be thrown.

Do not attempt to initialise the same backend multiple times, or initialise multiple backends simultaenously. Doing so will cause a RootAlreadyInitialised error to be thrown.

Avoid using FMTC in a seperate thread/Isolate. FMTC backends already make extensive use of multi-threading to improve performance.

If it is essential to use FMTC in a seperate thread, ensure that the initialisation is called in the thread where it is used. Be cautious of using FMTC manually across multiple threads simultaneously, as backends may not properly support this, and unexpected behaviours may occur.

Error Handling

One particular place where exceptions can occur more frequently is during initialisation. The code sample above includes a try/catch block to catch these errors. If an exception occurs at this point, it's likely unrecoverable (for example, it might indicate that the underlying database has been corrupted), and the best course of action is often to manually delete the FMTC root directory from the filesystem.

The default directory can be found and deleted with the following snippet (which requires 'package:path' and 'package:path_provider':

import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';

final dir = Directory(
  path.join(
    (await getApplicationDocumentsDirectory()).absolute.path,
    'fmtc',
  ),
);

await dir.delete(recursive: true);

// Then reinitialise FMTC

Uninitialisation

It is also possible to un-initialise FMTC and the current backend. This should be rarely required, but can be performed through the uninitialise method of the backend if required. Initialisation is possible after manual uninitialisation.

Backends

FMTC supports attachment of any custom storage mechanism, through an FMTCBackend. This allows users to pick their favourite database engine, or conduct in-memory testing.

Only one backend is built-into FMTC: the FMTCObjectBoxBackend. This backend uses the ObjectBox library to store data.

ObjectBox has a complex license model - the build time dependency is open-source, whilst the native library runtime only dependency is under a closed-source (but relatively relaxed) license (that is liable to change at ObjectBox's will).

This is not an issue for the majority of applications. However, ObjectBox is known to be (rightly or wrongly) banned as a dependency from apps on F-Droid (last checked September 2024).

Future updates to FMTC will implement alternative backends using other libraries, and the default/preferred backend may indeed change in future.

For more information, please see: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues/167.

flutter pub add flutter_map_tile_caching
android/app/build.gradle(.kts)
android {
    namespace = "*"
    compileSdk = flutter.compileSdkVersion
-   ndkVersion = flutter.ndkVersion
+   ndkVersion = <the version specified at the end of the error log>

    ...
}
pubspec.yaml
dependency_overrides:
    flutter_map_tile_caching:
        git:
            url: https://github.com/JaffaKetchup/flutter_map_tile_caching.git
            # ref: a commit hash, branch name, or tag (otherwise defaults to master)
macos/Runner/*.entitlements
<dict>
  <key>com.apple.security.application-groups</key>
  <array>
    <string>FGDTDLOBXDJ.demo</string>
  </array>  
...  
</dict>
migration info
supporters
(Proprietary) Licensing
GitHub Issues
flutter_map Discord server
[email protected]
documentation is still available

◉ 📲

Integrated Caching × Bulk Downloading

Get both dynamic browse caching that works automatically as the user browses the map, and bulk downloading to preload regions onto the user's device, all in one convenient, integrated API!

  • with and

  • when bulk downloading

  • Recoverable failed bulk downloads

◉ 🏃

Ultra-fast & Performant

No need to bore your users to death anymore! Bulk downloading is super-fast, and can even reach speeds of over 1000 tiles per second. Existing cached tiles can be displayed on the map almost instantly.

  • Multi-threaded setup to minimize load on main thread, even when browse caching

  • Streamlined internals to reduce memory consumption

  • Successfully downloaded tiles aren't re-downloaded when an unexpectedly failed download is recovered

◉ 🧩

Import & Export

Export and share stores, then import them later, or on other devices! You could even remote control your organization's devices, by pushing tiles to them, keeping your tile requests (& costs) low!

◉ 💖

Quick To Implement & Easy To Experiment

A basic caching implementation can be setup in four quick steps, and shouldn't even take 5 minutes to set-up. Check out our instructions.

Ready to experiment with bulk downloading, but don't want to make costly and slow tile requests? Check out the testing tile server included in the FMTC project: !

Lafayette GPS

Paid alternative license

Pitchero GPS

Paid alternative license
Cover

nventive

Paid alternative license
Cover

wemove digital solutions GmbH (contracting for Lower Saxony Ministry for the Environment, Energy and Climate Protection)

Free alternative license (non-GPL open-source)
💝Supporters
📃(Proprietary) Licensing
await FMTCStore('storeName').manage.create(maxLength: 1000);
await FMTCStore('storeName').manage.setMaxLength(null); // Disable max length
final maxLength = await FMTCStore('storeName').manage.maxLength;
https://nightly.link/JaffaKetchup/flutter_map_tile_caching/workflows/main/mainnightly.link
Latest Build Artifacts (thanks nightly)
Offline Mapping | v6 | flutter_map Docs
Quickstart
Testing Tile Server
Sponsor @JaffaKetchup on GitHub SponsorsGitHub
Sponsor Me via GitHub Sponsors

Bulk Downloading

FMTC provides the ability to bulk download areas of maps in one-shot, known as 'regions'. There are multiple different types/shapes of regions available.

Before using FMTC, especially to bulk download or import/export, ensure you comply with the appropriate restrictions and terms of service set by your tile server. Failure to do so may lead to any punishment, at the tile server's discretion.

This library and/or the creator(s) are not responsible for any violations you make using this package.

For example, OpenStreetMap's tile server forbids bulk downloading: https://operations.osmfoundation.org/policies/tiles. And Mapbox has restrictions on importing/exporting from outside of the user's own device.

For testing purposes, check out the testing tile server included in the FMTC project: Testing Tile Server.

Downloading is extremely efficient and fast, and uses multiple threads and isolates to achieve write speeds of hundreds of tiles per second (if the network/server speed allows). After downloading, no extra setup is needed to use them in a map (other than the usual Integrating With A Map).

Walkthrough

Before you can get started, make sure you've initialised FMTC & created one or more Stores!

1

Define a region

A region represents a geographical area only, not any of the other information required to start a download.

All types of region inherit from BaseRegion.

RectangleRegions are defined by a LatLngBounds: two opposite LatLngs.

final region = RectangleRegion(
    LatLngBounds(LatLng(0, 0), LatLng(1, 1)),
);

CircleRegions are defined by a center LatLng and radius in kilometers.

final region = CircleRegion(
    LatLng(0, 0), // Center coordinate
    1, // Radius in kilometers
);

If you instead have two coordinates, one in the center, and one on the edge, you can use 'latlong2's Distance.distance() method, as below:

final centerCoordinate = LatLng(0, 0); // Center coordinate
final region = CircleRegion(
    centerCoordinate,
    const Distance(roundResult: false).distance(
        centerCoordinate,
        LatLng(1, 1), // Edge coordinate
    ) / 1000; // Convert to kilometers
);

LineRegions are defined by a list of LatLngs, and a radius in meters.

This could be used to download tiles along a planned travel route, for example hiking or long-distance driving. Import coordinates from a routing engine, or from a GPX/KML file for maximum integration!

final region = LineRegion(
    [LatLng(0, 0), LatLng(1, 1), ...], // List of coordinates
    1000, // Radius in meters
);

This region may generate more tiles than strictly necessary to cover the specified region. This is due to an internal limitation with the region generation algorithm, which uses (rotated) rectangles to approximate the actual desired shape.

This type of region may consume more memory/RAM when generating tiles than other region types.

CustomPolygonRegions are defined by a list of LatLngs defining the outline of a simple polygon.

final region = CustomPolygonRegion(
    [LatLng(0, 0), LatLng(1, 1), ...], // List of coordinates
);

Polygons should not contain self-intersections. These may produce unexpected results.

Holes are not supported, however multiple CustomPolygonRegions may be downloaded at once using a MultiRegion.

MultiRegions are defined by a list of multiple BaseRegions (which may contain more nested MultiRegions).

When downloading, each sub-region specified is downloaded consecutively (to ensure that any start & end tile range defined is respected consistently.

Regions which overlap will still have the overlapping tiles downloaded for each region.

Multi region's advantage is that it reduces the number of costly setup and teardown operations. It also means that statistic measuring applies over all sub-regions, so it does not need to be managed indepedently.

It is also possible to reconstruct the region from a RecoveredRegion: RecoveredRegion.

2

Add information to make the region downloadable

BaseRegions must be converted to DownloadableRegions before they can be used to download tiles.

These contain the original BaseRegion, but also some other information necessary for downloading, such as zoom levels and URL templates.

final downloadableRegion = region.toDownloadable(
    minZoom: 1,
    maxZoom: 18,
    options: TileLayer(
        urlTemplate: '<your tile server>',
        userAgentPackageName: 'com.example.app',
    ),
),

The TileLayer passed to the options parameter must include both a urlTemplate (or WMS configuration) and a userAgentPackageName, unless it is only being used to check the number of tiles in the region.

3

(Optional) Count the number of tiles in the region

Before continuing to downloading the region, use countTiles() to count the number of tiles it will attempt to download. This is accessible through FMTCStore().download.

The method takes the DownloadableRegion generated above, and will return an int number of tiles. For larger regions, this may take a few seconds.

This figure will not take into account any skipped sea tiles or skipped existing tiles, as those are handled at the time of download.

4

Configure and start the download

To start the download, use the startForeground method on the existing store you wish to download to:

final (:downloadProgress, :tileEvents) =
  const FMTCStore('mapStore').download.startForeground(
    ...
  );

There are many options available to customize the download, which are described fully in the API reference:

The download starts as soon as the method is called; it does not wait for listeners.

5

Monitor the download outputs

Listening to the output streams of the download is something most apps will want to do, to display information to the user (unless operating in a headless mode or in the background).

There are two output streams returned as a record. One stream emits events that contain information about the download as a whole, whilst the other stream independently emits an event after the fetch/download of each tile in the region is attempted. See the API documentation for information on the exact emission frequencies of each stream. These are returned separately as the first stream emits events more frequently than the second, and this prevents tile events from needed to be repeated*.

An emitted TileEvent may refer to a tile for which an event has been emitted previously.

See the API documentation for more information.

6

(Optional) Control the download

Listening, pausing, resuming, or cancelling subscriptions to the output streams will not start, pause, resume, or cancel the download. It will only change whether the download emits updates.

Instead, there are methods available to control the download itself.

Pause/Resume

If your user needs to temporarily pause the download, with the ability to resume it at some point later (within the same app session), use pause and resume.

Pausing does not interrupt any tiles that are being downloaded when pause is invoked. Instead, the download will pause after the tile has been downloaded. pause's returned Future completes when the download has actually paused (or after resume is called whilst still pausing).

Pausing also does not cause the buffer to be flushed (if buffering is in use).

If listening to the DownloadProgress stream, an event will be emitted when pausing and resuming.

Use isPaused to check whether the download is currently paused.

Cancel

If your user needs to stop the download entirely, use cancel.

Cancelling does not interrupt any tiles that are being downloaded when cancel is invoked. The returned future completes when the download has stopped and been cleaned-up.

Any buffered tiles are written to the store before the future returned by cancel is completed.

It is safe to use cancel after pause without resumeing first.

Examples

StoreManagement class - flutter_map_tile_caching library - Dart API
Cover
export method - RootExternal class - flutter_map_tile_caching library - Dart API
ImportConflictStrategy enum - flutter_map_tile_caching library - Dart API
listStores property - RootExternal class - flutter_map_tile_caching library - Dart API
ImportResult typedef - flutter_map_tile_caching library - Dart API
import method - RootExternal class - flutter_map_tile_caching library - Dart API
StoreStats class - flutter_map_tile_caching library - Dart API
StoreMetadata class - flutter_map_tile_caching library - Dart API
RootRecovery class - flutter_map_tile_caching library - Dart API
RootStats class - flutter_map_tile_caching library - Dart API
Cover
Logo
Logo
Logo
Logo
Logo
Logo
Logo

Integrating With A Map

"Browse caching" occurs as the map loads tiles as the user interacts with it (or it is controlled by a MapController).

To inject the browse caching logic into flutter_map's tile loading process, FMTC provides a custom TileProvider: FMTCTileProvider.

Setup is quick and easy in many cases, but this guides through every step in the order in which it should be done to ensure best performance and all factors have been considered.

Remember that a store can hold tiles from more than one server/template URL.

Walkthrough

Before you can get started, make sure you've initialised FMTC & created one or more Stores!

1

Choose where to construct the tile provider

Where & how you choose to construct the FMTCTileProvider object has a major impact on performance and tile loading speeds, so it's important to get it right.

Minimize reconstructions of this provider by constructing it outside of the build method of a widget wherever possible. Because it is not a constant constructor, and it will be in a non-constant context (TileLayer), every rebuild will trigger a potentially expensive reconstruction.

However, in many cases, such as where one or more properties (as described in following stages) depends on inherited data (ie. via an InheritedWidget, Provider, etc.), this is not possible. In this case, read the tip in the API documentation carefully. In summary, you should construct as many arguments as possible outside of the build method, particularly a HTTP Client and any objects or callbacks which do not have a useful equality/hash code themselves.

2

Choose which stores it will interact with

The tile provider can interact in multiple ways with multiple stores at once, affording maximum flexibility. Defining how it interacts with these stores will be done in following stages, but you first need to define which stores it will interact with.

How exactly you need to define the stores depends on how much flexibility you need:

It is more common you will want to interact with just one or a defined set of stores at any one time. In this case, use the default constructor. You'll need to choose how (what strategy) it interacts with each store in following stages.

The parameters you will need to use will depend on how advanced your use-case is, but it will progress in a linear fashion:

  1. The mandatory stores argument takes a mapping of store names to the strategy to be used for that store.

  2. If you want to apply another strategy to all other available stores (whose names are not in the stores mapping), use the otherStoresStrategy argument.

  3. If you define that strategy, but you still want to disable interaction with some stores altogether, add these stores to the stores mapping with associated null values. (If otherStoresStrategy is not defined, stores mapped to null have no difference to if they were not included in the mapping at all.)

Ensure that all specified stores exist.

If you want it to interact with all available stores (those which have been created), all in the same way (see following stages), use the allStores named constructor, and that's it! FMTC will efficiently apply the strategy you choose in the next stage across all stores without you needing to track it yourself.

Remember that the provider can also read tiles from multiple stores, so this may not be necessary - but the option is there!

3

Choose how it will interact with the stores

The BrowseStoreStrategys tell FMTC how it should read, update, and create tiles in the store it is associated to.

In the allStores constructor, it is passed to allStoresStrategy and applied to all available stores (as described in the previous stage).

Otherwise, in the default constructor, one strategy is assigned to each store, plus optionally one to all other available stores (as described in the previous stage).

There are three possible strategies:

  • .read: only read tiles from the associated store

  • .readUpdate: read tiles, and also update existing tiles in the associated store, if necessary

  • .readUpdateCreate: read, update (if necessary), and create tiles in the associated store

4

Choose the preferred & fallback source for tiles

The BrowseLoadingStrategys (previously known as CacheBehaviors) tell FMTC the preferred source for tiles to be loaded from, and how to fallback if that source fails. It is passed to the loadingStrategy parameter.

There are three possible priorities:

Strategy
Preferred method
Fallback method

.cacheOnly

Cache

Failure

.cacheFirst

Cache

Network (URL)

.onlineFirst

Network (URL)

Cache

Standard tile provider

Network (URL)

Failure

The cacheOnly strategy essentially disables writing to the cache, and makes the chosen BrowseStoreStrategys above .read redundant.

The onlineFirst strategy may make tile loading appear slower when not connected to the Internet/network.

This is because the HTTP client may attempt to make the request anyway (Dart does not realise sometimes that the Internet is not available), in which case, the HTTP timeout set in the client must elapse before the tile is retrieved from the cache.

Customizing the interaction with otherStoresStrategy (if set)

The useOtherStoresAsFallbackOnly parameter concerns the behaviour of FMTC when a tile does not belong to any stores set in the stores mapping, but does belong to stores covered by otherStoresStrategy.

  • If false (as default), then the tile will be used without attempting the fallback method.

  • If true, then the tile will only be used if the fallback method fails.

This is not of concern if the strategy is onlineFirst, as if the always-attempted network fetch fails, the tile will always be used from the unspecified store.

Also see how this strategy influences tile updates in stage 6.

5

Ensure tiles are resilient to URL changes

To reference (enable correct creation/updating/reading of) tiles, FMTC uses a 'storage-suitable UID' derived from the tile's URL. Any one tile from the same server (style, etc. allowing) should have one storage-suitable UID which does not change.

On some servers, it may be acceptable for the UID to be the same as the tile URL. For example, the OpenStreetMap tile server URL for the tile at 0/0/0 will always be https://tile.openstreetmap.org/0/0/0.png. However, on some servers, the URL may change, but still point to the same desired tile. Consider the following URL: https://tile.paid.server/0/0/0.png?volatile_key=123. In this case, the URL requires an API key to retrieve the tile. If the UID was the same as the URL, but the key changes - for example, because it was leaked and refreshed - then FMTC would be unable to reference this tile when it encounters the same URL with the different key. This would mean the tile could not be read or updated, which may significantly impact your app's functionality.

To fix this, the urlTransformer parameter takes a callback which gets passed the tile's real URL, and should return a stable storage-suitable UID. For example, it should remove the offending query parameters.

The urlTransformer defined here should usually be the same as the transformer defined for a bulk download. Otherwise, tiles which have been bulk downloaded may not be able to be referenced, for example if an API key changes.

If the TileLayer used to start the bulk download uses an FMTCTileProvider with a defined urlTransformer as the tile provider, it will be used automatically, otherwise the bulk download also takes the urlTransformer directly.

If the offending part of the URL occurs as in the example above - as part of a query string - FMTC provides a utility callback which can be used as the transformer to remove the offending key & value cleanly. FMTCTileProvider.urlTransformerOmitKeyValues takes the tile URL as input, as well as a list of keys. It will remove both the key and associated value for each listed key. It may also be customized to use a different 'link' ('=') and 'delimiter' ('&') character, and it will remove any key<link>value found in the URL, not just from after the '?' character.

6

Configure tile updates

A tile will be updated in a store if all the following conditions are met:

The cachedValidDuration parameter can be used to set an expiry for all tiles written whilst it is set. Once a tile is expired, it will be flagged as needing updating. By default, there is no expiry set.

7

Configure other parameters

Basic hits & misses statistics (recordHitsAndMisses)

By default, every tile attempted during browsing records either a hit or miss.

A hit is recorded when a tile is read from the cache without attempting the network in all stores in which the tile exists & were present in the stores mapping (and not explicitly set null), or in which the tile exists if otherStoresStrategy was set.

In every other case, a miss is recorded in all stores present in the stores mapping (and not explicitly set null), or in all stores if otherStoresStrategy was set.

This information may not be useful or used in many apps, and so it may be disabled by setting it false. This will also improve performance (reduce tile loading times and device memory/storage operations). Additionally, more detailed and advanced metrics may be obtained by setting up a tileLoadingInterceptor (as below).

Handle tile load failures (errorHandler)

This feature is completely standalone to the TileLayer's errorImage.

By default, when a tile cannot be loaded, an FMTCBrowsingError is thrown containing some information about why the load failed. This is because something must be returned or thrown by internal ImageProvider.

However, it is possible to provide an error handler, which gets passed the failure as an argument, and may optionally return bytes (which must be decodable by Flutter). If it does return bytes, the error will not be thrown, and instead the bytes will be displayed in place of the tile image.

Intercept tile load events & info (tileLoadingInterceptor)

To track (eg. for debugging and logging) the internal tile loading mechanisms, an interceptor may be used.

For example, this could be used to debug why tiles aren't loading as expected (perhaps in combination with TileLayer.tileBuilder & ValueListenableBuilder as in the example app), or to perform more advanced monitoring and logging than the hit & miss statistics provide.

The interceptor consists of a ValueNotifier, which allows FMTC internals to notify & push updates of tile loads, and allows the owner to listen for changes as well as retrieve the latest update (value) immediately. The object within the ValueNotifier is a mapping of TileCoordinates to TileLoadingInterceptorResults.

Stores may also have a maxLength defined (the maximum number of tiles that store may hold). This is enforced automatically during browse caching.

8

And that's it! FMTC will handle everything else behind the scenes.

If you bulk download tiles, they'll be able to be used automatically as well.

Examples

A single store in a simple static configuration

This is the most simple case where one store exists, using the default constructor and no other parameters except a BrowseLoadingStrategy.

class _...State extends State<...> {
  final _tileProvider = FMTCTileProvider(
    stores: const {'mapStore': BrowseStoreStrategy.readUpdateCreate},
    loadingStrategy: BrowseLoadingStrategy.onlineFirst,
  );
  // and if "mapStore" is the only store, this could also be written as
  final _tileProvider = FMTCTileProvider.allStores(
    allStoresStrategy: BrowseStoreStrategy.readUpdateCreate,
    loadingStrategy: BrowseLoadingStrategy.onlineFirst,
  );
  
  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      options: MapOptions(),
      children: [
        TileLayer(
          urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
          userAgentPackageName: 'com.example.app',
          tileProvider: _tileProvider,
        ),
      ],
    );
  }
}

Two static named stores with a URL transformer

In this case, there are two stores which never change, which use different BrowseStoreStrategys. There is also a urlTransformer defined, using the utility method.

class _...State extends State<...> {
  final _tileProvider = FMTCTileProvider(
    stores: const {
      'store 1': BrowseStoreStrategy.readUpdateCreate,
      'store 2': BrowseStoreStrategy.read,
    },
    urlTransformer: (url) => FMTCTileProvider.urlTransformerOmitKeyValues(
      url: url,
      keys: ['access_key'],
    ),
  );
  
  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      options: MapOptions(),
      children: [
        TileLayer(
          urlTemplate: 'https://tile.paid.server/{z}/{x}/{y}.png?access_key={access_key}',
          userAgentPackageName: 'com.example.app',
          additionalOptions: const {
            'access_key': '123',
          },
          tileProvider: _tileProvider,
        ),
      ],
    );
  }
}

Stores set from a Provider/Selector with a URL transformer

Note that the URL transformer callback and HTTP client have been defined outside of the FMTCTileProvider constructor (which must lie within the build method because it depends on inherited data).

Defining the URL transformer this way instead of an anonymous function ensures that the caching key works correctly, which improves the speed of tile loading.

Defining the HTTP client (although it is technically optional) ensures it remains open even when the provider is being repeatedly reconstructed, which means it does not have to keep re-creating connections to the tile server, improving tile loading speed. Note that it is not closed when the widget is destroyed: this prevents errors when the widget is destroyed whilst tiles are still being loaded, and there is very little potential for memory or performance leaks.

class _...State extends State<...> {
  late final _httpClient = IOClient(HttpClient()..userAgent = null);
  String _urlTransformer(String url) =>
      FMTCTileProvider.urlTransformerOmitKeyValues(
        url: url,
        keys: ['access_key'],
      );
  
  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      options: MapOptions(),
      children: [
        Selector<GeneralProvider, Map<String, BrowseStoreStrategy?>>(
          selector: (context, provider) => provider.stores,
          builder: (context, stores, _) => 
            TileLayer(
              urlTemplate: 'https://tile.paid.server/{z}/{x}/{y}.png?access_key={access_key}',
              userAgentPackageName: 'com.example.app',
              additionalOptions: const {
                'access_key': '123',
              },
              tileProvider: FMTCTileProvider(
                stores: stores,
                urlTransformer: _urlTransformer,
                httpClient: _httpClient,
              ),
            ),
      ],
    );
  }
}

Using multiple stores alongside otherStoresStrategy, and explicitly disabling a store

class _...State extends State<...> {
  final _tileProvider = FMTCTileProvider(
    stores: const {
      'store 1': BrowseStoreStrategy.readUpdateCreate,
      'store 2': BrowseStoreStrategy.read,
      // 'store 3' implicitly gets `.readUpdate`,
      'store 4': null, // disabled
    },
    otherStoresStrategy: BrowseStoreStrategy.readUpdate,
  );
  
  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      options: MapOptions(),
      children: [
        TileLayer(
          urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
          userAgentPackageName: 'com.example.app',
          tileProvider: _tileProvider,
        ),
      ],
    );
  }
}
Two static named stores with a URL transformer
Logo
Logo
Logo
Logo
Logo
DownloadProgress class - flutter_map_tile_caching library - Dart API
toDownloadable method - BaseRegion class - flutter_map_tile_caching library - Dart API
BaseRegion class - flutter_map_tile_caching library - Dart API
TileEvent class - flutter_map_tile_caching library - Dart API
startForeground method - StoreDownload class - flutter_map_tile_caching library - Dart API
Logo
Logo
Logo
Logo
Logo