Все мы в определенные периоды жизни принимаем лекарства или витамины. При этом, зачастую, необходимо строго соблюдать режим приема. Но все мы люди. И лично я регулярно забываю принять лекарство.
В помощь себе и другим забывчивым товарищам я и написал Android — приложение «Medication — Прием лекарств».
Идея написать приложение-«напоминалку» возникла у меня примерно полтора года назад. Чуть больше года назад в Google Play появилась первая версия приложения (идея должна хотя бы немного «настояться»).
С тех пор я веду неспешную разработку приложения в свободное от основной работы время.
Но прежде чем что-либо разрабатывать, необходимо обосновать эту разработку, поэтому:
- Выбор платформы. «Напоминалка» должна быть всегда под рукой. Также она должна быть «активной», т.е. обращать на себя внимание пользователя. Простой бумажкой не обойдешься. Получается, что телефон подходит для этого лучше всего. Как платформу выбрал Android в силу наличия у меня соответствующего устройства.
- Поиск подобных приложений под Android на тот момент результатов не дал (может, плохо искал?). Рассмотрел аналоги под iOS, но по скриншотам сложно оценить функционал приложения, а iPhone для «пощупать» на руках отсутствовал.
- Было желание разработать приложения под Android в целях изучения SDK и возможностей платформы (причем именно приложение, а не игру).
В принципе, задача не выглядит такой сложной:
- Ведение списка медикаментов
- Ведение нескольких времен приема для каждого медикамента
The devil is in the details
А теперь напишу окончательный список функций (сравните с исходной постановкой задачи):
- Установка наименования и примечания для приема медикамента
- Учет времени приема медикамента
- Каждый день
- По дням недели
- Каждые… дней
- В определенный день
- Указание даты начала приема медикамента
- Учет даты окончания приема
- Возможность отметить прием медикамента незадолго до наступления времени приема
- Учет дозировки
- Стандартной, для каждого приема
- Специальной для отдельного приема
- Мелодия уведомления
- Смена мелодии в настройках
- Установка специальной мелодии для отдельных медикаментов
- Функция «не напоминать»
- Система напоминаний
- Повторные напоминания, если не был отмечен прием лекарства
- Учет смены системного времени и часового пояса
- Учет возможного выключения устройства
- Архив
- Перенос настроенных приемов в архив
- Перенос из архива
- Виджет
- Следит за всем назначениями или только за определенным
- Несколько фоновых изображений
- Сообщения об ошибках
- Поддержка промо-кодов
- Мультиязычность
- Русский
- Английский
- Французский
Список дополнительных функций в Про-версии:
- Учет запасов
- Сигнализация о заканчивающихся запасах
- Расчет срока окончания запасов
- Просмотр журнала приема
- Полная очистка журнала
- Очистка журнала за определенный день
- Профили
- Отображение назначенных медикаментов для определенного профиля
Целью я поставил скорейший выпуск приложения, пусть там были далеко не все функции, описанные выше.
Опишу те моменты, которые хотел бы выделить.
Контроль версий
Любая разработка нуждается в контроле версий. Уже давно прошли те времена, когда я в этих целях использовал копии папок или файлов (а что новее «project_old» или «project_old_old»?).
На данный момент для контроля версий я предпочитаю Mercurial. Простота, функциональность и некоторая защита от дурака. Сервер разворачивается буквально одной командой… Стоп! Какой сервер? Спасибо bitbucket.org за «Unlimited private code repositories».
Механизм напоминаний
Для напоминаний в Android есть специальный менеджер — AlarmManager. Хотя и стандартный Java-класс Timer с этим прекрасно справлялся. Но только пока телефон не заснет. AlarmManager — наш выбор.
База данных
Лично я от БД хотел ORM. Но и тянуть большую ORM в проект не хотелось — место необходимо экономить, т.к. перенести приложение на SD-карту нельзя (нарушается работа системы напоминаний). А основной памяти и так мало. По этой же причине отказался и от использования HoloEverywhere и многих других библиотек в проекте.
В итоге, как систему хранения я выбрал protobuf + SharedPreferences.
Protocol Buffers — язык описания данных, предложенный Google, как альтернатива XML. Предполагается, что Protocol Buffers проще и легче, чем XML (из википедии).
Имеет генераторы классов для большинства языков программирования. Для работы необходимо сгенерировать классы и добавить библиотеку в проект (весом в 23КБ, что меня вполне устроило).
Как показала практика, скорость работы protobuf вполне достаточная, так что основная система хранения построена на нем и сейчас. Объекты protobuf сериализуются в массив байт, который затем перегоняется в BASE64-строку, и сохраняются в SharedPreferences.
Хотя все время хочу перевести систему хранения полностью на SQLite. Наверное, в дальнейшем остановлюсь на гибридном варианте: сериализация protobuf и хранение в SQLite, вместо SharedPreferences.
«Эталонное» время
Система напоминаний тесно связана со временем. Время я храню как количество миллисекунд с 1.01.1970 00:00:00.000 GMT в типе long.
Java такую систему хранения полностью поддерживает.
Хранить полноценный DateTime не позволил protobuf. Там просто нет соответствующего типа данных (впрочем, как и в SQLite, так что я ничего не потерял).
От хранения времени в строке я отказался:
- Занимает много места.
- Долго разбирается и преобразуется обратно.
Стоит учитывать, что при смене часового пояса, время «25.09.2013 00:00» преобразуется или в «25.09.2013 01:00», или в «24.09.2013 23:00». Т.е. если я в программе предполагаю, что эта дата всегда указывает на полночь (00:00), то после смены часового пояса это уже не так. В принципе, часовой пояс меняется не так уж и часто. Но тем не менее, пользователи путешествуют, переезжают, отдыхают и т.д. Это штатная ситуация в работе программы. И портить напоминания для тех 5% пользователей, которые сменили часовой пояс, нельзя.
Выкрутился я следующими способами:
- При смене часового пояса автоматически рассчитывается дата следующего приема исходя из текущего, нового времени и времени прошлого приема. Скажем, был прием на 2 часа дня. После перелета это время — уже 9 утра. Но в силу перерасчета мы напомним о приеме в 2 часа дня, как этого и ожидает пользователь.
- Добавил в программу понятие «эталонного времени». Т.е. я сохранил как константу строку «2010-01-01» и при смене часового пояса преобразую ее в long и сохраняю в SharedPreferences. Если значение этой даты в миллисекундах изменилось, то я, зная старое значение, могу рассчитать разницу в часовых поясах и правильно подправить даты в напоминаниях, которые завязаны на дни: дата окончания приема, дата начала действия периода приема и т.д.
«Вытащить» разницу в часовых поясах из соответствующего BroadcastReceiver не удалось. Если кто знает способ — подскажите.
Не так давно вылезла и проблема: при переходе на летнее время событие о смене часового пояса не генерируется. Кто-нибудь может подсказать выход в данной ситуации? Гугл тут не сильно помог. Нашел рекомендацию помимо события «android.intent.action.TIMEZONE_CHANGED» подписываться на «android.intent.action.TIME_SET» и «android.intent.action.DATE_CHANGED». Но еще не проверял данное решение (полгода на исправление есть).
Расчет запасов
Оценка прогноза «на сколько хватит запасов лекарства?» — важная и нужная функция. Расчет такого прогноза — задача нетривиальная. Если бы у меня приемы хранились только как «в 8:00 каждый день», то все было бы просто. Имеем общий объем, имеем количество приемов в день, имеем объем одного приема.
Но расписание приема у меня можно задавать достаточно гибко. И каждые N дней, и по дням недели, и т.д. Причем для каждого времени можно указать с какого числа это время действует. И это не говоря уже про то, что для каждого приема можно установить персональную дозировку. В итоге прогноз запасов строился таким образом: я проигрывал «прием» по дням до тех пор, пока у меня не закончатся запасы. Отсюда и ограничение — в запасе не более 10000 базовых доз (кстати, расчет запасов идет не более чем на 10000 дней). Для реальных пользователей не думаю, что это как-то скажется. А вот как защита от дурака — пригодится.
ACRA
Для слежением за CrashReport-ами я использую ACRA. Удобная, мощная и вместе с тем простая штука.
Bug-Report
В своем приложении я добавил функцию «сообщить об ошибке».
Спасибо ACRA за возможность отправки BugReport-а буквально в пару строчек кода:
String email = ((TextView) findViewById(R.id.email)).getText().toString();
String msg = ((TextView) findViewById(R.id.msg)).getText().toString();
ACRA.getErrorReporter().putCustomData("email", email);
ACRA.getErrorReporter().putCustomData("msg", msg);
ACRA.getErrorReporter().handleSilentException(new Throwable("BUG REPORT"));
И все. Вместе со стандартными BugReport-ами придут и пользовательские сообщения.
При этом придут и настроенные приемы, что позволят очень легко воспроизвести ошибку.
Промо-коды
В целях повышения уровня продаж про-версии я решил воспользоваться промо-кодами.
Нашел для этого сервис IndieYard, но их free-тариф меня не устроил (1 приложение, 2 компании, до 500 активаций). Других аналогов не обнаружил. В общем, решил написать свой велосипед. Кстати, добавил функцию чтения промо-кода по QR-коду (можно воспользоваться любым считывателем QR-кодов).
В итоге получился JSON-RPC сервис, написанный на PHP (я с ним немного знаком + был сервер, где уже развернуто необходимое окружение). Для JSON-RPC на Android есть готовые библиотеки, хотя и самостоятельно организовать общение не так уж и сложно. Можно было бы и REST, но JSON-RPC мне показался уместнее (да, если честно, то подход RPC мне ближе, чем REST).
Думаю, более подробную статью напишу позже (когда выпущу полноценный сервис или хотя бы его бетку). Сейчас же это 1 функция проверки промо-кода и «админка» в виде Adminer (аналог phpMyAdmin). Это даже не альфа-версия. О надежности решения говорить не приходится.
Из интересного, что можно отметить — использование базы SQLite.
Почему SQLIte? Проще переносить сервис из локального окружения в «боевое». Меньше мороки с резервным копированием (хотя тут под вопросом). Да и производительности хватает.
Изначально производительность была порядка 70 операций проверки промо-кода (каждая включает 3 select-запроса, 1 update, 1 insert) на локальной машине (i5, SSD). Но после оптимизации база позволила держать уже около 800 операций/сек. Оптимизация заключалась в добавлении индексов по используемым в запросах полям.
На сервере показатель несколько меньше: сначала было 30 операций/сек, оптимизация подняла до 120 (сервер облачный в Selectel, на самом «древнем» пуле).
К слову, тот же сервис, но использующий PostgeSQL (через PDO), давал порядка 150 операций в секунду. Оптимизация базы практически не сказалась: 165 запросов (для локальной машины, на сервере показатель очень непостоянный — от 50 до 120 запросов в секунду).
Интересен показатель MongoDB. 1900 запросов в секунду на локальной машине.
Но не стоит забывать, что MongoDB в первую очередь ориентирована на скорость.SQLIte ориентирован на надежность. А PostgeSQL в дефолтной настройке ориентирован на минимум потребления ресурсов. Так что, прямо скажем, сравнение не самое честное.
- Используется WAL-журнал. Позволяет иметь несколько «читателей». Но запись одновременно только в 1 потоке (кстати, этот же режим значительно ускоряет работу и в однопоточном режиме).
- Установил Busy Timeout в 5000. Говорит ждать 5 секунд своей очереди на запись.
- Обернул запись в БД в транзакции. Причем использовал «BEGIN IMMEDIATE». Блокирует запись начиная с момента вызова. С обычным «BEGIN» возникли серьезные проблемы в многопоточной работе (как раз для многопоточной работы предназначены «BEGIN IMMEDIATE» и «BEGIN EXCLUSIVE»). Более подробно о проблеме здесь.
Аудитория
Для того, чтобы максимально удовлетворять пользователей, разработчик должен «знать» своих пользователей «в лицо». Для сбора статистики я встроил Google Analystics. И теперь могу смело сказать — приложением пользуются буквально во всем мире! Правда, в Африке больше всего «белых пятен», где не ступала нога человека с моей программой на телефоне.
Рейтинг (ответы)
Отзывы пользователей важны. Без обратной связи сделать качественное приложение, на мой взгляд, невозможно.
Для поддержания рейтинга рекомендую отвечать на отзывы с замечаниями. Как правило, пользователь просто не может найти нужную ему функцию. Иногда — просит новый функционал. Как правило, после ответа разработчика, пользователь меняет свой ответ и ставит выше балл.
Монетизация
Еще до начала разработки я выработал свою позицию в данном вопросе. Рекламу я отказался вставлять категорически. Причин было несколько:
- Лично мне не нравится реклама в приложениях. Зачем мне разрабатывать приложение, которое мне не нравится?
- Что делать, если появится реклама «чудо-средства от всех проблем» в приложении? А пользователь, доверяющий мне, как разработчику, возьмет и купит на последние средства. Понятное дело, «сам виноват», но тем не менее…
Для монетизации я решил выпустить платную версию приложения, с расширенными функциями. Изначально я хотел делать отдельную, платную версию приложения. Но возникло несколько вопросов:
- Что делать, если пользователь покупает про-версию, а назначения настроены в старой? В принципе, можно эти данные перенести, но и в таком случае остается очень много вопросов. К тому же, на мой взгляд, хороший программист — это «ленивый» программист, а что может быть лучше, чем ничего не делать? И ошибок меньше, и логика поведения прозрачнее (недавно такой способ разработки назвали «темным путем»).
- Я хотел использовать промо-коды. А промо-коды должны разблокировать про-функции. Получается, функционально, про-версия не должна отличаться от обычной? А зачем тогда про-версию выделять в отдельное приложение?
Остается In-App Purchase. Но и он меня не устроил:
- Какой версии использовать? In-App Purchase очень зависит от версии приложения маркета. А у меня идет поддержка устройств начиная с версии Android 1.6, а значит будет полный зоопарк версий.
- Достаточно легко взламывается (судя по отзывам, сам не пробовал).
В итоге опять написал свой велосипед (видел подобное у Note Everything). Я сделал плагин, который надо покупать в Google Play как отдельное приложение. А плагин уже и разблокирует про-функции в приложении. В итоге широчайшая поддержка устройств, вкупе с минимумом усилий (да, да, темный путь зовет меня).
Пиар
Я особо не пиарил приложение. Еще в самом начале опубликовал на форуме 4pda тему с обсуждением. Собирался воспользоваться программой «Программы поддержки наших разработчиков» там же. Но ее не так давно отменили. В принципе, я не силен в пиаре и маркетинге. Поэтому хочу провести небольшой эксперимент.
Напишите обзор приложения в своем блоге или на любом другом ресурсе, и получите промо-код для разблокировки полной версии для Вас и Вашей семьи (5 активаций). Связаться со мной можно по адресу AndSDev@gmail.com.
Перед публикацией жду письмо, в котором не забудьте указать:
- Ресурс, на котором собираетесь публиковать.
- Язык.
- 3 промо-кода для разблокировки отдельных про-функций (журнал (лог) приемов, профили, слежение за запасами). Для включения в обзор для читателей.
Я зарегистрирую промо-коды и вышлю временный промо-код на про-версию для написания обзора.
После публикации опять жду письмо. Не забудьте ссылку на обзор — для коллекции ). С меня код на про-версию (5 активаций).
Сюрприз
Для тех, кто дочитал до конца, я решил сделать небольшой сюрприз. Как я уже говорил, я внедрил поддержку промо-кодов в свое приложение. А раз есть поддержка, то почему бы ей не воспользоваться?
Промо-коды (действительны до конца 2013 года):
Разблокировка лога приема: HABRALOG (QR)
Разблокировка профилей: HABRAPROFILE (QR)
Разблокировка слежения за запасами медикаментов: HABRASTOCK (QR)
Ссылка на скачку приложения: Medication (QR)
Плагин про-версии: Medication PRO (QR)
Итог
За 3 месяца продаж приложение принесло чуть больше 100$ прибыли (примерно по 1$ в день). Окупает ли это полтора года неспешной разработки? Нет, не окупает.
Окупает разработку другое. Множество людей благодарят меня по почте и в отзывах Google Play за разработку этого приложения. Этого хватает на то, чтобы я продолжал поддержку данного приложения.
Автор: SabMakc