Недавно меня опять заклампили. Я живу в Европе, и здесь вместо штрафов за неправильную парковку и эвакуаторов “клампят” — заковывают колесо твоего автомобиля в цепи. Чтобы выбраться, нужно звонить по телефону, платить круглую сумму и ждать мужика с ключами, который снимет цепь. Это долго, унизительно и порой (зависит от района) грабительски дорого.
В тот день я опоздал везде. Ожидая звенящего ключами работника, я размышлял, насколько глупо попался. Забегался, оставил машину на полчаса вместо максимальных бесплатных 20 минут — ровно на 21-й минуте и попался. Не повезло, полосатый фургончик парковщиков стоял недалеко, и они моментально среагировали. Ловили меня и до этого, по разным причинам: забывал, истекал оплаченный срок, а иногда и просто не мог найти свою машину в лабиринте улиц.
“Для всего должно быть приложение” — подумал я и начал копаться в апп сторе. После вороха сомнительных результатов у меня поубавилось уверенности, и я решил уточнить: “для всего должно быть приложение на андроид”. После чего нашел свой хуавей и полез в недра плей стора. Оттуда на меня высыпалось еще больше мусора, и я, утопая в корявых поделках, плюнул. Либо я ищу как-то не так, либо не существует удобного и понятного трекера парковки. Вывод простой: если у нас чего-то нет, давайте сделаем это сами.
На фоне у меня уже пилился долгострой, отнимавший почти все свободное время. Я решил взять паузу и в промежутке собрать другой проектик. Прикинув желаемый функционал, мне представился срок в месяц-полтора, и, забегая вперед, скажу, что в принципе вышло даже быстрее. По итогу получилось компактное и чистое приложение на обе платформы, очень удобное. Сейчас сам им пользуюсь и предлагаю попробовать вам.
Итак, чего же я хотел бы от трекера парковки? Чего мне всегда не хватает, когда я попадаю на штраф? Подумав, я сформулировал следующие вводные:
В качестве технологической базы я выбрал Flutter, так как уже работаю с ним какое-то время и считаю неплохим. Для маршрутов решил использовать HERE API, тоже уже не в первый раз, а для карты — Mapbox. Набросал себе несколько вариантов интерфейса, выбрал самый минималистичный и включился в работу.
В принципе, про сам процесс разработки рассказать можно не особо много. Серверной части тут нет, простейший клиент, состоящий по сути из одной большой вьюшки в разных состояниях. На первой странице ты либо выбираешь пресет — 1, 2, 4, 6 часов — либо слайдером настраиваешь свой срок парковки. Попробовав это вживую, я понял, что слайдер достаточно груб, и добавил ему кнопки “+” и “-“. Забавным и довольно неочевидным нюансом тут стало то, что, пока ты выбираешь — время идет. Так что обновлять эту вьюшку нужно в реальном времени. Выбрав срок, нажимаем на единственную кнопку внизу, и данные по локации и времени сохраняются, а парковка стартует. Важно заметить, что я принципиально храню все данные на борту, ничего ни на какой сервер не передается. В дальнейшем это, кстати, создаст мне трудности, но про это позже.
Вторая страница — это, собственно, таймер парковки. Я перепробовал тут кучу вариантов: начинал с дико перегруженных инфой прототипов, после чего итеративно избавлялся и упрощал до предела. Мне очень хотелось, чтобы статус парковки изображался визуально, а не сухими цифрами, поэтому главным элементом этой страницы наряду с картой стала кольцевая диаграмма. Она показывает истекшее время, время ходьбы и оставшееся время. Если ты находишься не рядом с машиной и время ходьбы ненулевое — появляется кнопка запуска навигации, а карта показывает примерный маршрут до парковки. Вот, собственно, и все.
Не смотря на визуальную простоту, этот экран изрядно потрепал мне нервы. Надо понимать, что пока парковка активна, происходит одновременно ряд процессов:
Все это происходит в реальном времени и должно моментально отображаться на интерфейсе. Диаграмма должна плавно перестраиваться, карта двигаться и перерисовывать маршрут, при риске опоздать или истечении парковки соответствующее предупреждение тоже должно выскакивать. Более того: при закрытии и открытии приложения нужно это все корректно восстановить. И наконец страшное: весь функционал должен уметь работать в фоновом режиме, ведь большинство своей жизни приложение проведет именно на фоне. Такие дела.
Не буду описывать реализацию в деталях, скажу лишь, что флаттеровская работа со стейтами очень достойная. Там, где в чистом андроиде получилась бы перегруженная объектно-ориентированная конструкция, у меня вышло просто и элегантно. Нормальные понятные стейты, толковая привязка вьюшек к моделям — в задачах подобных этой флаттер сияет. Впрочем, не обошлось и без типичных косяков: например, у местной библиотеки работы с картами отсутствует анимированный переход на новые координаты. Пришлось лезть в ее код, вытаскивать какие-то приватные методы и дописывать свой костыль. Не исключено, что этот функционал скоро допишут, но в этом весь флаттер — при всей его привлекательности будьте готовы к таким вот открытиям.
С остальным вроде вышло все гладко. Собственно, остального там было и немного: пара диалогов, да интро-страница, для которой моя девушка (она художница) набросала приятные анимации. Наконец, я перевел все приложение на русский, включая карту. А вот дальше началась боль: уведомления.
Для меня возможность присылать умные уведомления была ключевой для приложения — без нее нечего и городить. Однако, когда я начал работать над этой функцией, я понял, что сам себе наступил на ногу. Проблем на самом деле было две. Первая — флаттер, который не умеет из коробки работать с низкоуровневыми апи девайсов: нужно писать прокладку в виде плагина, которая содержит два нативных кода и абстракцию на дарте. Вторая проблема — моя принципиальная позиция, что все должно выполняться на борту. Итак, суммируем, что надо. Надо, чтобы приложение на фоне:
Чувствуете масштаб трагедии? Понятно, что не существует технической возможности это делать, если приложение совсем убито. Но допустим, оно просто свернуто, а экран выключен. Маршрут и оценку времени ходьбы мне дает апи HERE, значит ли это, что приложение будет спамить реквестами на фоне? Сколько кода вообще будет при этом прогоняться? Как у нас с памятью и расходом энергии?
Я сел думать. Начал с малого: технически у флаттера с недавнего времени есть возможность выполнять код в фоновом режиме, она называется isolate. Если все правильно написать, то можно построить логику специфическим образом, чтобы не вся она выполнялось на фоне, а какой-то определенный функционал. Ведь в свернутом режиме нам не нужно рисовать интерфейсы или обновлять стейты — нас интересует голая логика вычисления времени, а эта логика довольно компактная и дешевая. При этом запускать ее должна не смена геопозиции, как было у меня в первоначальном варианте, а некий внутренний таймер. Потому что уведомление должно прилететь, даже если ты не двигаешься, а сидишь на месте.
Ну хорошо, мы написали легкий код, который умеет прогоняться на фоне и управляется таймером. Как нам его тригерить в рамках нативных приложений и передавать туда данные геолокации? Оказалось, по-разному.
Логика андроида и айфона в вопросах выполнения кода на бекграунде кардинально разная. Для айфона оно отсутствует как таковое: фоновый код может вызвать только системное событие определенного рода, например проигрывание потокового аудио или — к счастью для меня — обновление локации. Для этого достаточно запросить пермишен и в нативной части флатеровского плагина подписаться на событие. Оттуда через специфический канал связи (MethodChannel) мы вызываем коллбэк в дарте, передаем ему геоданные и запускаем фоном нужную нам логику. Красота.
У андроида это работает по-другому. Технически у него есть возможность выполнять код фоном без привязки к системным событиям — для этого надо описать сервис. Он висит отдельно от приложения либо совсем на фоне, либо на форграунде (там он дает от себе знать прикрепленным сообщением в шторке). Казалось бы, проблема решена: в нативной части плагина описываем фоновый сервис, который трекит локацию и дергает дартовский код. Однако есть нюанс. С андроида версии 8.0 поменялось отношение к таким сервисам: теперь, если приложение свернуто в данный момент, его фоновые сервисы адски режутся осью в целях экономии энергии. И, если твой код осуществляет, например, бекап данных, то тебе в принципе все равно, в какой именно момент ось пустит его работать — главное, чтобы сработал. А вот в моем случае нам необходимо отслеживать изменение локации практически в реальном времени, и тайминг здесь критичен. Поэтому единственный выход, это пускать сервис в форграунд. Описываем небольшое прикрепленное в шторке уведомление, подписываемся на локацию, через MethodChannel передаем ее в дарт. Вроде все работает.
Кстати, возвращаясь к вопросам памяти, расхода энергии и количества реквестов — код прогоняется при смещении локации в 50 метров, либо по таймеру. Причем сам он очень легкий, а реквесты проходят не через меня, а идут напрямую на апи HERE. Я сам не получаю никаких данных и нигде у себя не сохраняю. Вообще подобная реализация очень хорошо описана тут, это блог одного из инженеров флаттера — там и код прилагается. Его пример несколько про другое, но общий подход понятен, так что рекомендую почитать.
Вот, собственно, и все. За месяц свободного времени у меня получилось приложение, отлично решающее конкретную задачу. Надеюсь, эта статья будет кому-то интересна, а приложение спасет от штрафов и стресса. Поглядеть и поругать можно тут: Android, iOS.
Всех благодарю за внимание.
В тот день я опоздал везде. Ожидая звенящего ключами работника, я размышлял, насколько глупо попался. Забегался, оставил машину на полчаса вместо максимальных бесплатных 20 минут — ровно на 21-й минуте и попался. Не повезло, полосатый фургончик парковщиков стоял недалеко, и они моментально среагировали. Ловили меня и до этого, по разным причинам: забывал, истекал оплаченный срок, а иногда и просто не мог найти свою машину в лабиринте улиц.
“Для всего должно быть приложение” — подумал я и начал копаться в апп сторе. После вороха сомнительных результатов у меня поубавилось уверенности, и я решил уточнить: “для всего должно быть приложение на андроид”. После чего нашел свой хуавей и полез в недра плей стора. Оттуда на меня высыпалось еще больше мусора, и я, утопая в корявых поделках, плюнул. Либо я ищу как-то не так, либо не существует удобного и понятного трекера парковки. Вывод простой: если у нас чего-то нет, давайте сделаем это сами.
На фоне у меня уже пилился долгострой, отнимавший почти все свободное время. Я решил взять паузу и в промежутке собрать другой проектик. Прикинув желаемый функционал, мне представился срок в месяц-полтора, и, забегая вперед, скажу, что в принципе вышло даже быстрее. По итогу получилось компактное и чистое приложение на обе платформы, очень удобное. Сейчас сам им пользуюсь и предлагаю попробовать вам.
Идея
Итак, чего же я хотел бы от трекера парковки? Чего мне всегда не хватает, когда я попадаю на штраф? Подумав, я сформулировал следующие вводные:
- Простое. В идеале однокнопочное. Минимум отвлекающих функций: никаких профилей, истории парковок, поддержки нескольких авто одновременно и тд.
- С уведомлениями. Я бы не забывал про машину, если бы приложение мне напоминало: братан, у тебя там парковка между прочим, и она вот-вот истечет.
- Учитывать время ходьбы. Это простейшая мысль, которую я нигде не видел воплощенной. Мне не надо знать, что парковка истечет через 5 минут, если я в получасе ходьбы от нее. Мне нужно напомнить так, чтобы я успел дойти до машины за пару минут до конца и уехать.
- Маршрут и навигация к парковке. Навигация, естественно, через гугл или эппл карты, а вот маршрут можно и в своем приложении показывать. Это поможет, если ты оставил машину в незнакомом районе, таймер тикает, а ты бегаешь кругами по одинаковым улицам.
В качестве технологической базы я выбрал Flutter, так как уже работаю с ним какое-то время и считаю неплохим. Для маршрутов решил использовать HERE API, тоже уже не в первый раз, а для карты — Mapbox. Набросал себе несколько вариантов интерфейса, выбрал самый минималистичный и включился в работу.
Разработка
В принципе, про сам процесс разработки рассказать можно не особо много. Серверной части тут нет, простейший клиент, состоящий по сути из одной большой вьюшки в разных состояниях. На первой странице ты либо выбираешь пресет — 1, 2, 4, 6 часов — либо слайдером настраиваешь свой срок парковки. Попробовав это вживую, я понял, что слайдер достаточно груб, и добавил ему кнопки “+” и “-“. Забавным и довольно неочевидным нюансом тут стало то, что, пока ты выбираешь — время идет. Так что обновлять эту вьюшку нужно в реальном времени. Выбрав срок, нажимаем на единственную кнопку внизу, и данные по локации и времени сохраняются, а парковка стартует. Важно заметить, что я принципиально храню все данные на борту, ничего ни на какой сервер не передается. В дальнейшем это, кстати, создаст мне трудности, но про это позже.
Вторая страница — это, собственно, таймер парковки. Я перепробовал тут кучу вариантов: начинал с дико перегруженных инфой прототипов, после чего итеративно избавлялся и упрощал до предела. Мне очень хотелось, чтобы статус парковки изображался визуально, а не сухими цифрами, поэтому главным элементом этой страницы наряду с картой стала кольцевая диаграмма. Она показывает истекшее время, время ходьбы и оставшееся время. Если ты находишься не рядом с машиной и время ходьбы ненулевое — появляется кнопка запуска навигации, а карта показывает примерный маршрут до парковки. Вот, собственно, и все.
Не смотря на визуальную простоту, этот экран изрядно потрепал мне нервы. Надо понимать, что пока парковка активна, происходит одновременно ряд процессов:
- время идет, и истекает таймер
- ты перемещаешься, и время пути меняется
Все это происходит в реальном времени и должно моментально отображаться на интерфейсе. Диаграмма должна плавно перестраиваться, карта двигаться и перерисовывать маршрут, при риске опоздать или истечении парковки соответствующее предупреждение тоже должно выскакивать. Более того: при закрытии и открытии приложения нужно это все корректно восстановить. И наконец страшное: весь функционал должен уметь работать в фоновом режиме, ведь большинство своей жизни приложение проведет именно на фоне. Такие дела.
Не буду описывать реализацию в деталях, скажу лишь, что флаттеровская работа со стейтами очень достойная. Там, где в чистом андроиде получилась бы перегруженная объектно-ориентированная конструкция, у меня вышло просто и элегантно. Нормальные понятные стейты, толковая привязка вьюшек к моделям — в задачах подобных этой флаттер сияет. Впрочем, не обошлось и без типичных косяков: например, у местной библиотеки работы с картами отсутствует анимированный переход на новые координаты. Пришлось лезть в ее код, вытаскивать какие-то приватные методы и дописывать свой костыль. Не исключено, что этот функционал скоро допишут, но в этом весь флаттер — при всей его привлекательности будьте готовы к таким вот открытиям.
С остальным вроде вышло все гладко. Собственно, остального там было и немного: пара диалогов, да интро-страница, для которой моя девушка (она художница) набросала приятные анимации. Наконец, я перевел все приложение на русский, включая карту. А вот дальше началась боль: уведомления.
Уведомления
Для меня возможность присылать умные уведомления была ключевой для приложения — без нее нечего и городить. Однако, когда я начал работать над этой функцией, я понял, что сам себе наступил на ногу. Проблем на самом деле было две. Первая — флаттер, который не умеет из коробки работать с низкоуровневыми апи девайсов: нужно писать прокладку в виде плагина, которая содержит два нативных кода и абстракцию на дарте. Вторая проблема — моя принципиальная позиция, что все должно выполняться на борту. Итак, суммируем, что надо. Надо, чтобы приложение на фоне:
- Постоянно мониторило твою геолокацию,
- Проверяло, сколько минут ходьбы до парковки
- Сверялось с истекающим таймером
- Если есть риск не успеть вернуться — стреляло уведомление
Чувствуете масштаб трагедии? Понятно, что не существует технической возможности это делать, если приложение совсем убито. Но допустим, оно просто свернуто, а экран выключен. Маршрут и оценку времени ходьбы мне дает апи HERE, значит ли это, что приложение будет спамить реквестами на фоне? Сколько кода вообще будет при этом прогоняться? Как у нас с памятью и расходом энергии?
Я сел думать. Начал с малого: технически у флаттера с недавнего времени есть возможность выполнять код в фоновом режиме, она называется isolate. Если все правильно написать, то можно построить логику специфическим образом, чтобы не вся она выполнялось на фоне, а какой-то определенный функционал. Ведь в свернутом режиме нам не нужно рисовать интерфейсы или обновлять стейты — нас интересует голая логика вычисления времени, а эта логика довольно компактная и дешевая. При этом запускать ее должна не смена геопозиции, как было у меня в первоначальном варианте, а некий внутренний таймер. Потому что уведомление должно прилететь, даже если ты не двигаешься, а сидишь на месте.
Ну хорошо, мы написали легкий код, который умеет прогоняться на фоне и управляется таймером. Как нам его тригерить в рамках нативных приложений и передавать туда данные геолокации? Оказалось, по-разному.
Логика андроида и айфона в вопросах выполнения кода на бекграунде кардинально разная. Для айфона оно отсутствует как таковое: фоновый код может вызвать только системное событие определенного рода, например проигрывание потокового аудио или — к счастью для меня — обновление локации. Для этого достаточно запросить пермишен и в нативной части флатеровского плагина подписаться на событие. Оттуда через специфический канал связи (MethodChannel) мы вызываем коллбэк в дарте, передаем ему геоданные и запускаем фоном нужную нам логику. Красота.
У андроида это работает по-другому. Технически у него есть возможность выполнять код фоном без привязки к системным событиям — для этого надо описать сервис. Он висит отдельно от приложения либо совсем на фоне, либо на форграунде (там он дает от себе знать прикрепленным сообщением в шторке). Казалось бы, проблема решена: в нативной части плагина описываем фоновый сервис, который трекит локацию и дергает дартовский код. Однако есть нюанс. С андроида версии 8.0 поменялось отношение к таким сервисам: теперь, если приложение свернуто в данный момент, его фоновые сервисы адски режутся осью в целях экономии энергии. И, если твой код осуществляет, например, бекап данных, то тебе в принципе все равно, в какой именно момент ось пустит его работать — главное, чтобы сработал. А вот в моем случае нам необходимо отслеживать изменение локации практически в реальном времени, и тайминг здесь критичен. Поэтому единственный выход, это пускать сервис в форграунд. Описываем небольшое прикрепленное в шторке уведомление, подписываемся на локацию, через MethodChannel передаем ее в дарт. Вроде все работает.
Кстати, возвращаясь к вопросам памяти, расхода энергии и количества реквестов — код прогоняется при смещении локации в 50 метров, либо по таймеру. Причем сам он очень легкий, а реквесты проходят не через меня, а идут напрямую на апи HERE. Я сам не получаю никаких данных и нигде у себя не сохраняю. Вообще подобная реализация очень хорошо описана тут, это блог одного из инженеров флаттера — там и код прилагается. Его пример несколько про другое, но общий подход понятен, так что рекомендую почитать.
Результат
Вот, собственно, и все. За месяц свободного времени у меня получилось приложение, отлично решающее конкретную задачу. Надеюсь, эта статья будет кому-то интересна, а приложение спасет от штрафов и стресса. Поглядеть и поругать можно тут: Android, iOS.
Всех благодарю за внимание.