Hey! Меня зовут Дмитрий Лёвочкин, я Flutter разработчик в Friflex и автор блога Дневник Flutter разработчика. Мы в Friflex занимаемся разработкой мобильных приложений, и одна из наших ключевых отраслей – ритейл. Сложно представить мобильное приложение крупного ритейлера без карты внутри. В этой статье я расскажу, какие преимущества есть у Яндекс Карт и как быстро интегрировать их в ваше приложение.

Статья состоит из двух частей:

  1. Теоретическая. Плюсы и минусы использования Яндекс Карт.

  2. Практическая. Напишем приложение, в котором отобразим текущее местоположение пользователя. В случае ошибки будем выводить Москву как город по умолчанию. 

Если вы занимаетесь написанием приложений на Flutter, то, скорее всего, знаете, что для интеграции карт в ваше приложение есть несколько основных пакетов:

В контексте ситуации, сложившейся в нашей стране, у Яндекс карт есть ряд преимуществ:

  1. Если вы планируете публиковать приложение в AppGallery, вам не нужно писать разделение для GMS (Google Mobile Services) и HMS (Huawei Mobile Services) сервисов. Яндекс Карты без проблем работают с Huawei. Если вы будете использовать Google Карты, вам потребуется разделить сервисы, так как ваше приложение не может одновременно поддерживать HMS и GMS сервисы (согласно политике Google Play).

  2. В случае возникновения проблем с работой сервисов Google в РФ, ваше приложение будет стабильно работать с картами Яндекса.

  3. По регионам России информация точнее, чем у Google Карт.

  4. Более знакомый интерфейс для русскоязычного пользователя.

  5. Не возникнет проблем с оплатой сервиса, если вы решите перейти на платный тариф.

Вы можете разделить сервисы, использовать Google Карты для публикации приложения в App Store и Google Play, а Яндекс Карты для публикации в AppGallery. Но это добавит проблем при сборке. Нужно будет каждый раз менять карты.

Минусы при работе с Яндекс Картами:

  1. Значительно увеличивает размер APK. Для нативной разработки есть две версии пакета – full и lite. Lite решает эту проблему, но во Flutter она не работает. Issue здесь: https://github.com/Unact/yandex_mapkit/issues/194

  2. Всегда работает только с одним языком. Из-за нативных ограничений после запуска приложения язык нельзя изменить.

  3. Документация не адаптирована под Flutter и сейчас она только для нативной разработки: https://yandex.com/dev/maps/mapkit/doc/intro/concepts/about.html

К счастью, во Flutter библиотеке хорошо описан процесс подключения, а на github можно найти множество примеров использования.
В Telegram есть чат разработчиков по yandex_mapkit, там можно получить ответы на вопросы.

С теоретической частью закончили, теперь давайте перейдем к практике.

Интеграция Яндекс Карт во Flutter

Задача –  интегрировать Яндекс Карты, отобразить текущее местоположение пользователя.
Для подключения карт нам потребуется пакет yandex_mapkit
Для определения местоположения будем использовать geolocator (имеет Flutter Favorite). 
Вы можете использовать любой другой сервис, но нужно иметь в виду, что geolocator использует сервисы Google на Android, и качество геолокации может быть значительно ниже для Huawei. Чтобы избежать такой проблемы, для HMS лучше отдельно использовать их плагин huawei_location
Добавляем оба плагина в pubspec.yaml файл нашего Flutter Android проекта:

yandex_mapkit: ^3.2.0
geolocator: ^9.0.2

Местоположение определяется по текущей широте и долготе.
У геолокатора и у Яндекс Карт есть свой класс для широты и долготы. Но у одного одни модели, а у другого  – другие. Нам нужна своя модель без завязки на реализации сторонних сервисов. 
Для этого создадим класс AppLatLong со значениями типа double. Также создадим класс MoscowLocation с координатами Москвы (будем выводить Москву, если не получится определить текущее местоположение):

class AppLatLong {
 final double lat;
 final double long;

const AppLatLong({
   required this.lat,
   required this.long,
 });
}

class MoscowLocation extends AppLatLong {
 const MoscowLocation({
   super.lat = 55.7522200,
   super.long = 37.6155600,
 });
}

Создаем новый файл app_location.dart, в котором создаем абстрактный класс AppLocation с тремя методами:

abstract class AppLocation {
 Future<AppLatLong> getCurrentLocation();

 Future<bool> requestPermission();

 Future<bool> checkPermission();
}

Далее создаем сервис LocationService, в котором реализовываем интерфейс AppLocation. Этот сервис будет отвечать за получение текущего местоположения пользователя. Создадим переменную defLocation с дефолтными координатами.
Расписываем его методы:

class LocationService implements AppLocation {
 final defLocation = const MoscowLocation();
}

У geolocator есть свои методы, которые мы здесь используем:

  • getCurrentPosition() – для определения текущей геопозиции (широта и долгота);

  • requestPermission() – для запроса на разрешение использования сервиса местоположения;

  • checkPermission() проверяет, разрешил ли пользователь доступ к геопозиции устройства.

В методе getCurrentLocation() в случае успешного определения местоположения, мы вернем текущие широту и долготу. В случае ошибки вернем широту и долготу Москвы. Координаты нужны, чтобы дальше можно было отобразить их на карте. 

@override
Future<AppLatLong> getCurrentLocation() async {
 return Geolocator.getCurrentPosition().then((value) {
   return AppLatLong(lat: value.latitude, long: value.longitude);
 }).catchError(
   (_) => defLocation,
 );
}

В методе requestPermission() мы делаем повторный запрос на доступ к сервису геопозиции. Если пользователь разрешил доступ к геопозиции, вернем true. В случае ошибки вернем false.

@override
Future<bool> requestPermission() {
 return Geolocator.requestPermission()
     .then((value) =>
         value == LocationPermission.always ||
         value == LocationPermission.whileInUse)
     .catchError((_) => false);
}

Метод checkPermission() практически копирует предыдущий метод и возвращает булевое значение в зависимости от того, предоставил ли пользователь доступ к определению геопозиции.

@override
Future<bool> checkPermission() {
 return Geolocator.checkPermission()
     .then((value) =>
         value == LocationPermission.always ||
         value == LocationPermission.whileInUse)
     .catchError((_) => false);
}

Сервис для определения геопозиции готов! Создаем новый файл map_screen.dart и StatefulWidget в нем – MapScreen:

class MapScreen extends StatefulWidget {
 const MapScreen({Key? key}) : super(key: key);

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

class _MapScreenState extends State<MapScreen> {
 @override
 Widget build(BuildContext context) {
   return Scaffold();
 }

Подключаем пакет yandex_mapkit:
Вначале нам нужно получить MapKit mobile SDK key. Для этого идем https://developer.tech.yandex.com/ , выбираем MapKit mobile SDK key и заполняем простую форму (бесплатный тариф). После успешного заполнения копируем наш ключ:

Для подключения Android:
Идем в android > app > build.gradle и добавляем: 

implementation 'com.yandex.android:maps.mobile:4.2.2-full'

Идем в android > app > src > main > AndroidManifest.xml и добавляем два разрешения:

 <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Идем в файл android > app > src > main > kotlin или java у вас > MainActivity.kt и добавляем код (берем в readme https://pub.dev/packages/yandex_mapkit) в зависимости от того java у вас или kotlin. Добавляем наш ключ, который получили выше, в 11 строку. Не пугаемся подчеркиваний и ошибок, просто закрываем файл:

Не забудьте добавить первую строку с названием. 


Для подключения iOS
Идем в iOS> Runner > AppDelegate.swift и добавляем вначале импорт: 

import YandexMapsMobile

Затем:

YMKMapKit.setApiKey("YOUR_API_KEY")

Не забудьте добавить ваш ApiKey, который получили ранее.

В iOS > podfile добавляем

platform :ios, '12.0':  

platform :ios, '12.0': В iOS  > Runner > Info.plist обязательно нужно добавить:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Доступ к геолокации пользователя необходим для отображения текущей геопозиции</string>

Это требование Apple, им нужно описание. Необходимо добавить в описание, для чего запрашиваем геопозицию. Иначе можно не пройти проверку в App Store. 


yandex_mapkit для Android и iOS подключили.
Создаем новый файл map_screen.dart и StatefulWidget в нем – MapScreen:

class MapScreen extends StatefulWidget {
 const MapScreen({Key? key}) : super(key: key);

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

class _MapScreenState extends State<MapScreen> {
 final mapControllerCompleter = Completer<YandexMapController>();

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Текущее местоположение'),
     ),
     body: YandexMap(
       onMapCreated: (controller) {
         mapControllerCompleter.complete(controller);
       },
     ),
   );
 }
}

Добавляем контроллер через Completer, который будем заполнять дальше, в onMapCreated 
Используем методы из LocationService:

Future<void> _initPermission() async {
 if (!await LocationService().checkPermission()) {
   await LocationService().requestPermission();
 }
 await _fetchCurrentLocation();
}

Future<void> _fetchCurrentLocation() async {
 AppLatLong location;
 const defLocation = MoscowLocation();
 try {
   location = await LocationService().getCurrentLocation();
 } catch (_) {
   location = defLocation;
 }
 _moveToCurrentLocation(location);
}

Метод _initPermission() проверяет, предоставил ли пользователь разрешения на определение геопозиции. Если не предоставил, то делаем запрос на разрешение доступа к геопозиции. После этого вызываем метод _fetchCurrentLocation() для установки координат.
Метод _fetchCurrentLocation()  получает необходимые координаты для метода _moveToCurrentPosition(). В случае ошибки, или если он не сможет определить текущее местоположение, вернет координаты Москвы. 


Напишем метод _moveToCurrentPosition(). Это основной метод, который и будет показывать местоположение пользователя на карте:

Future<void> _moveToCurrentLocation(
 AppLatLong appLatLong,
) async {
 (await mapControllerCompleter.future).moveCamera(
   animation: const MapAnimation(type: MapAnimationType.linear, duration: 1),
   CameraUpdate.newCameraPosition(
     CameraPosition(
       target: Point(
         latitude: appLatLong.lat,
         longitude: appLatLong.long,
       ),
       zoom: 12,
     ),
   ),
 );
}

Метод принимает координаты высоты и широты, которые мы получили выше. 
Ждем получения mapControllerCompleter и далее, используя методы .moveCamera()  и .newCameraPosition(), по полученным координатам анимированно переносим фокус на текущее местоположение.  
Параметр zoom: 12 задает отдаление ближе/дальше, так можно отобразить более точные координаты местоположения. 


Осталось только добавить метод _initPermission().ignore() для запроса разрешений и установления координат в initState() MapScreen, и реализация готова.
.ignore() нужен здесь для безопасной обработки и игнорирования Future метода _initPermission()

@override
void initState() {
 super.initState();
 _initPermission().ignore();
}

Запускаем приложение:

Hidden text

Отлично, задача выполнена! Мы получили текущее местоположение пользователя:)
Мы интегрировали Яндекс Карты в приложение и научились определять текущее местоположение пользователя. В следующей части статьи добавим маркер для более точных координат текущего местоположения и покажем необходимые объекты рядом, например, магазины.

Если у вас остались вопросы по интеграции Яндекс Карт во Flutter, оставляйте их в комментариях!
Код проекта доступен по ссылке.

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