Всем привет! Меня зовут Анна Ахлёстова, я Flutter-разработчик в Friflex. Ранее мы обсудили, как использовать инструменты yandex_mapkit в Flutter-проекте. В этой статье рассмотрим еще один плагин для работы с картографическими сервисами – flutter_map, изучим его возможности, преимущества перед аналогами и реализуем простой проект в качестве примера.

Если вы начнете искать удобный, адаптированный под Flutter картографический сервис, основными результатами поиска будут GoogleMaps API и Yandex Mapkit SDK.

Их популярность в целом оправдана – они предоставляют обширный список функций: начиная от простого отображения самой карты и объектов на ней и заканчивая функциями геокодирования и навигации. Помимо этого сами карты достаточно точны и детализированы, что немаловажно для конечного пользователя. 

Их главный недостаток – стоимость коммерческих тарифов. Если заказчики обладают достаточным бюджетом для систематической оплаты сервиса карт, проблема автоматически решается. Однако когда разработка ПО осуществляется для небольших организаций или даже стартапов, заказчик может не иметь возможности тратить на это деньги.

В этом случае возможное решение проблемы – использовать бесплатные функции у вышеперечисленных сервисов. При этом придется постоянно следить за соблюдением всех условий бесплатного тарифа, а также не допускать превышения максимального лимита запросов/пользователей.

Для Flutter-проектов есть еще одно решение – применять OpenStreetMap. 

Этот картографический сервис является абсолютно бесплатным для всех пользователей. Создатели заявляют, что все загруженные в него данные доступны каждому пользователю. Подробнее о политике и самом сервисе вы можете почитать здесь.

Для внедрения OpenStreetMap в Flutter-проект существует плагин flutter_map

На мой взгляд, его самое большое преимущество – наличие полной систематизированной документации. В ней вы можете найти не только подробное описание основных объектов библиотеки, но и примеры их использования. Кроме этого, здесь указаны самые популярные библиотеки, дополняющие текущий функционал. А также, что немаловажно, документация обновляется с каждым мажорным обновлением плагина.

flutter_map работает по принципу слоев – каждый отдельный тип объектов должен быть помещен на отдельный слой. Само изображение карты отображается с помощью слоя TileLayer. Далее по принципу стека на него могут быть наложены слои объектов карты:

  • MarkerLayer – для отображения маркеров точек на карте;

  • PolygonLayer – для выделения зон в виде произвольной фигуры на карте по списку точек;

  • PolylineLayer – для построения линии (например, маршрута);

  • CircleLayer – для выделения круглых зон относительно заданной центральной точки на карте;

  • OverlayImageLayer – для наложения изображений поверх карты;

  • Attribution Layer (RichAttributionWidget) – для ссылки на источники используемых данных.

Плагин дает возможность дополнять текущий функционал сторонними библиотеками, что описано в документации. Уже реализованы библиотеки для настройки кластеризации маркеров (flutter_map_marker_cluster, flutter_map_supercluster), кэширования карты для использования ее в оффлайн-режиме (flutter_map_cache, flutter_map_tile_caching), парсинга объектов из json напрямую в список объектов карты (flutter_map_geojson), кастомизации анимаций карты (flutter_map_animations) и т.д.

Также библиотека дает возможность разработчику выбирать основной сервер. Документация описывает работу нескольких наиболее известных – Mapbox, Thunderforest и прочих, а также предлагает целый список доступных для подключения серверов. Кроме этого, при необходимости, вы можете развернуть свой собственный сервер и подключиться к нему.

Создадим простое приложение, чтобы наглядно увидеть, как работает сервис. Подробно о процессе подключения плагина в проект можно почитать здесь.

Создадим страницу MapScreen в файле map_screen.dart. На странице отобразим основной виджет библиотеки FlutterMap. В него передадим контроллер MapController и основные настройки MapOptions (здесь укажем точку, относительно которой карта будет отцентрирована при инициализации, а также уровень начального масштабирования карты).

В поле children передадим виджет TileLayer, который необходим для отображения основного изображения карты, полученного по ссылке в параметре urlTemplate. Также этот виджет принимает в себя параметр userAgentPackageName — здесь необходимо указать название вашего приложения.

Файл map_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';

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

 @override
 State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
 late final MapController _mapController;

 List<LatLng> get _mapPoints => const [
       LatLng(55.755793, 37.617134),
       LatLng(55.095960, 38.765519),
       LatLng(56.129038, 40.406502),
       LatLng(54.513645, 36.261268),
       LatLng(54.193122, 37.617177),
       LatLng(54.629540, 39.741809),
     ];

 @override
 void initState() {
   _mapController = MapController();
   super.initState();
 }

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

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Map Screen'),
     ),
     body: FlutterMap(
       mapController: _mapController,
       options: const MapOptions(
         initialCenter: LatLng(55.755793, 37.617134),
         initialZoom: 5,
       ),
       children: [
         TileLayer(
           urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
           userAgentPackageName: 'com.example.flutter_map_example',
         ),
       ],
     ),
   );
 }
}

Получим следующий результат:

Далее создадим метод _getMarkers() для генерации маркеров на карте:

/// Метод генерации маркеров
List<Marker> _getMarkers(List<LatLng> mapPoints) {
 return List.generate(
   mapPoints.length,
   (index) => Marker(
     point: mapPoints[index],
     child: Image.asset('assets/icons/map_point.png'),
     width: 50,
     height: 50,
     alignment: Alignment.center,
   ),
 );
}

Для отображения маркеров в поле children виджета FlutterMap добавим виджет MarkersLayer, в который передадим список маркеров из вышеописанного метода. 

Файл map_screen.dart:

body: FlutterMap(
       mapController: _mapController,
       options: const MapOptions(
         initialCenter: LatLng(55.755793, 37.617134),
         initialZoom: 5,
       ),
       children: [
         TileLayer(
           urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
           userAgentPackageName: 'com.example.flutter_map_example',
         ),
         MarkerLayer(
           markers: _getMarkers(_mapPoints),
         ),
       ],
     ),

Результат в приложении:

К сожалению, с помощью стандартных инструментов flutter_map нельзя реализовать кластеризацию маркеров. Чтобы добавить эту функцию, можно использовать сторонние библиотеки, например, flutter_map_marker_cluster или flutter_map_supercluster.

Применяем в нашем примере библиотеку flutter_map_marker_cluster. 

После стандартного подключения пакета, в зависимости pubspec.yaml, создадим виджет _ClusterMarker, который будет отображать количество объединенных маркеров в кластере.

Виджет _ClusterMarker в файле map_screen.dart:

/// Виджет для отображения кластера
class _ClusterMarker extends StatelessWidget {
 const _ClusterMarker({required this.markersLength});

 /// Количество маркеров, объединенных в кластер
 final String markersLength;

 @override
 Widget build(BuildContext context) {
   return Container(
     decoration: BoxDecoration(
       color: Colors.blue[200],
       shape: BoxShape.circle,
       border: Border.all(
         color: Colors.blue,
         width: 3,
       ),
     ),
     child: Center(
       child: Text(
         markersLength,
         style: TextStyle(
           color: Colors.blue[900],
           fontWeight: FontWeight.w700,
           fontSize: 18,
         ),
       ),
     ),
   );
 }
}

Создадим виджет MarkerClusterLayerWidget. В его поле options передадим основные настройки MarkerClusterLayerOptions.

Класс MarkerClusterLayerOptions предназначен для кастомизации создаваемых кластеров. С его помощью можно настроить не только внешний вид кластеров, но и возможность взаимодействовать как с кластерами, так и с маркерами. Также объект принимает в себя список маркеров, входящих в кластеры.

Сформируем такой виджет:

MarkerClusterLayerWidget(
           options: MarkerClusterLayerOptions(
             size: const Size(50, 50),
             maxClusterRadius: 50,
             markers: _getMarkers(_mapPoints),
             builder: (_, markers) {
               return _ClusterMarker(
                 markersLength: markers.length.toString(),
               );
             },
           ),
         ),

Передадим его в общий список виджетов карты, вместо виджета MarkerLayer и получим следующий результат:

Код
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
import 'package:latlong2/latlong.dart';

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

 @override
 State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
 late final MapController _mapController;

 List<LatLng> get _mapPoints => const [
       LatLng(55.755793, 37.617134),
       LatLng(55.095960, 38.765519),
       LatLng(56.129038, 40.406502),
       LatLng(54.513645, 36.261268),
       LatLng(54.193122, 37.617177),
       LatLng(54.629540, 39.741809),
     ];

 @override
 void initState() {
   _mapController = MapController();
   super.initState();
 }

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

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Map Screen'),
     ),
     body: FlutterMap(
       mapController: _mapController,
       options: const MapOptions(
         initialCenter: LatLng(55.755793, 37.617134),
         initialZoom: 5,
       ),
       children: [
         TileLayer(
           urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
          userAgentPackageName: 'com.example.flutter_map_example'
         ),
         MarkerClusterLayerWidget(
           options: MarkerClusterLayerOptions(
             size: const Size(50, 50),
             maxClusterRadius: 50,
             markers: _getMarkers(_mapPoints),
             builder: (_, markers) {
               return _ClusterMarker(
                 markersLength: markers.length.toString(),
               );
             },
           ),
         ),
       ],
     ),
   );
 }
}

/// Метод генерации маркеров
List<Marker> _getMarkers(List<LatLng> mapPoints) {
 return List.generate(
   mapPoints.length,
   (index) => Marker(
     point: mapPoints[index],
     child: Image.asset('assets/icons/map_point.png'),
     width: 50,
     height: 50,
     alignment: Alignment.center,
   ),
 );
}

/// Виджет для отображения кластера
class _ClusterMarker extends StatelessWidget {
 const _ClusterMarker({required this.markersLength});

 /// Количество маркеров, объединенных в кластер
 final String markersLength;

 @override
 Widget build(BuildContext context) {
   return Container(
     decoration: BoxDecoration(
       color: Colors.blue[200],
       shape: BoxShape.circle,
       border: Border.all(
         color: Colors.blue,
         width: 3,
       ),
     ),
     child: Center(
       child: Text(
         markersLength,
         style: TextStyle(
           color: Colors.blue[900],
           fontWeight: FontWeight.w700,
           fontSize: 18,
         ),
       ),
     ),
   );
 }
}

Теперь приложение выглядит следующим образом:

Подводя итоги, могу сказать, что flutter_map показался мне отличным решением для простых проектов. Плагин дает возможность быстро и без особых усилий внедрить картографический сервис в приложение. Он собрал в себе все базовые функции, а те, что в него не вошли, могут быть легко реализованы с помощью сторонних библиотек. Это мы выяснили практическим путем.

Делитесь опытом работы с flutter_map и задавайте интересующие вас вопросы в комментариях. Код проекта доступен здесь.

Комментарии (3)


  1. stryder123451
    24.11.2023 15:38

    А навигацию по нему можно делать?


  1. lcat
    24.11.2023 15:38

    Данный плагин, вероятно, берет тайлы карты напрямую с серверов OSM. Однако данные сервера, в первую очередь, предназначены для самой разработки и картирования и есть прецеденты блокирования особо рьяных потребителей трафика. Поэтому коммерческим пользователям с большими объемами потребления трафика рекомендуется поднимать свои тайловые сервера.


  1. vanmhit
    24.11.2023 15:38

    Вот только работа с линией перемены дат у него страдает. Например, маршрут Токио - Гонолулу выглядит так.

    Токио - Гонолулу
    Токио - Гонолулу

    И через 180 меридиан не даёт скроллить.