Что можно успеть за новогодние праздники? Как я смог выяснить, очень много. Даже если у тебя двое детей и куча родни, которую хочется навестить. Не получается только закончить статью для хабра. Она, зараза, растягивается на весь январь.
5ого числа я оказался с ноутбуком и 2мя свободными днями. Прикинув что можно сделать полезного, я решил сделать какое-нибудь мобильное приложение быстрее, чем его создатели. Поскольку 2 дня это не то, чтобы много, нужно было найти небольшое, популярное приложение с разговорчивыми авторами.
В App Store нашлось приложение "Транжира". Это небольшая программа для записи своих трат и доходов. В конце месяца по ним можно сделать вывод почему опять не хватило до зарплаты. На 5ое число оно входило в Топ-10 в разделе «Финансы». На iphones.ru нашлась dev-story.
В своей статье ребята пишут: «После сдачи проекта у нашей команды обычно есть 3-4 полусвободных дня (таковы правила компании), и уже через час над приложением нашего дизайнера работали 4 человека. Первым в “бой” ринулся product manager, подсказав несколько ключевых моментов по позиционированию приложения. За ним программисты и даже office manager.». Это меня обнадежило и я стал думать как уложиться в 2 дня.
UPD 1: приложение обновилось во второй половине января. Сравнивая с моим приложением стоит ориентироваться на скриншоты и функционал из dev-story.
UPD 2: иконка «Транжиры» удивительно похожа на иконку платных постов из «4 правил автора» на хабре:
картинка.
У меня уже был опыт мобильной разработки на C# и Cocoa. Посколько время тратил личное, хотелось использовать его с максимально пользой. Даже если успеть приложение не получиться, то хотя бы «вкусить» нового фреймворка или языка.
Я работал в DevExpress с 2006 по 2011 года и с тех пор следил за их анонсами. От них до сих пор приходит What's New на мою почту. Соблазнить на покупку что-ли хотят? У них появился мобильный js-фреймворк на базе Cordova/PhoneGap. Писать его начали уже после моего увольнения, поэтому что это за зверь я не знал и было интересно попробовать.
Исследовательская компания Gartner пишет, что по состоянию на август 2013 большинство корпоративных мобильных приложений было написано на PhoneGap или продуктах на его основе (вроде Kony). Мой личный, потребительнский опыт подсказывает что это не так. Но вдруг я заблуждаюсь?
С JavaScript и HTML я знаком понаслышке. Верстаю со stackoverflow подмышкой, умею писать простенькие jQuery селекторы. Знаю, где находится её документация. В общем, это огромная дыра в моих знаниях. Очень хотелось если не закрыть её, то хотя бы получить какой-то опыт.
В итоге у меня должно было получиться кросс-платформенное приложение. Это позволило бы мне получить преимущество над разработчиками Транжиры и произвести больше добавленной пользы за доступное время. У меня была всего одна пара рук и использовать её хотелось наиболее эффективно. На одной чаше весов была потенциальная эффективность фреймворка, на другой — отсутствие опыта в js-разработке у меня. Я понадеялся, что фреймворк перевесит и решил попробовать.
Мне удобно писать с VCS, поэтому сейчас я попытаюсь восстановить и описать по часам мой прогресс.
Готовые приложения вы можете скачать здесь:
iOS версия на ревью
Не уверен, что могу выложить в публичный доступ репо. В нем лежат фото, купленные на стоке и сторонние библиотеки каждая со своей лицензией. Написанный мною код, вы можете увидеть ниже, либо на свой риск заглянув внутрь бандла приложения.
Место действия: Тула, Дата: 5 января 2014
+20 минут Ушло на установку node.js и Cordova CLI
+10 минут Скачал шаблон приложения от Cordova. Добавил шаблон из PhoneJS. Создал Git-репозиторий, зарегистрировал его в WebStorm. Добавил запись в httpd.conf, чтобы была возможность отлаживать и проверять работу приложения в браузере.
+38 минут Поменял неймспейс приложения на io.nikitin.ThriftBox. Настроил навигацию. PhoneJS является MVC-фреймворком. Каждый экран приложения представлен набором из html разметки (вью) и функцией-фабрикой вью-модели. В простейшем случае, это
<div data-options="dxView : { name: 'home', title: 'Home' } ">
<div data-options="dxContent : { targetPlaceholder: 'content' } ">
// Содержимое вью
</div>
</div>
плюс
ThriftBox.home = function (params) { // Параметры запроса из uri
return {}; // Объект вью-модели
};
В дальнейшем, одно с другим увязывается с помощью knockout-биндингов. Времени в притык, поэтому оставляю два экрана: ввод трат и отчет трат по месяцам.
+4 часа 20 минут Случился первый «затык». Не получилось быстро сделать разметку цифровых кнопок для ввода трат.
В исходном приложении на главном экране огромная клавиатура, похожая на калькулятор или приложение-звонилку.
Оказалось, что даже «в лоб», с помощью тега table сделать такую клавиатуру не получается. На retina-iphone бордюры в 1 пиксел между кнопками меняли свой цвет после нажатия кнопок. Разница в цвете линий на хорошем экране телефона была очень заметна. Пришлось думать, как победить.
Изучал вопрос и опробовал вариант верстки на div'ах. С ними не получилось добиться, чтобы рамка была ровно 1 пиксел шириной и все кнопки были одинакового размера на экранах разного разрешения. Спустя 3 часа баг был оставлен как есть и я двинулся дальше.
+28 минут Убран индикатор нажатия ссылки на iOS. iOS отрисовывает серый индикатор нажатия вокруг ссылок и объектов с обработчиком onclick. Поскольку у меня был свой индикатор нажатия (кнопка становилась темнее), этот серый индикатор мне был не нужен. Проблема решилась с помощью события dxAction:
было:
<td data-bind="click: function() { buttonPress('1') }">1</td>
стало:
<td data-bind="dxAction: function() { buttonPress('1') }">1</td>
Это событие — расширенная вариация события click: в качестве обработчика оно поддерживает uri навигации между вью и корректно отрабатывает в скроллируемой область.
+14 минут Обработчик buttonPress из предыдущего примера, валидация введенного числа.
var number = ko.observable(null);
var isValidNumber = ko.computed(function() {
return number() && parseFloat(number()) > 0;
});
......
function buttonPress(button) {
if (button) {
if (number())
number(number() + button);
else
number(button);
} else if (number())
number(number().substr(0, number().length - 1));
}
var viewModel = {
number: number,
isValidNumber: isValidNumber,
viewShowing: viewShowing,
buttonPress: buttonPress
};
.....
+8 минут Добавил FastClick.js, который убирает задержку перед срабатыванием события click на телефонах. По умолчанию, мобильный браузер откладывает вызов обработчика click, чтобы убедиться, что не будет double-tap. Внешне это проявляется как подтормаживающее приложение. Вы быстро нажимаете кнопками, а они реагируют с ощутимой задержкой на нажатия. Авторы FastClick.js вешаются на touchstart и выстраивают свою логику обработки нажатий.
Забегая вперед скажу, что добавление этой библиотеки было ошибкой. Почему — читайте далее.
+4 минуты Ввел ограничение на длину числа, которое может ввести пользователь. Подкорректировал размер шрифтов, чтобы все отлично выглядело и не вылезало за границы области ввода.
+58 минут Выбор категории траты. Под областью ввода добавил прокручивающуюся область с кнопками доступных категорий. Видео.
Заняло меньше, чем могло бы. Внутри PhoneJS нашел подходящий компонент dxTileView. Из коробки получил нужный мне внешний вид и кинетик-прокрутку. С последней пришлось бы помучится, если делать её самому. Особенно здорово, что ребята из DevExpress включили её только для iOS, где она — системная, но выключили для Android, где такой прокрутки нет.
<div class="category" data-bind="dxTileView: {
dataSource: categories,
itemMargin: 0,
baseItemHeight: 72,
baseItemWidth: 72,
listHeight: 72
}">
<div class="tile"
data-options="dxTemplate: { name:'item' }"
data-bind="css: { selected: $parent.category() == $data },
click: function() { $parent.category($data) }">
<div><img data-bind="attr: { src: image }"/></div>
<div data-bind="text: name"></div>
</div>
</div>
Было уже 19:40 и я решил закончить.
Место действия: Тула, Дата: 6 января 2014
+3 часа 9 минут Хранение данных на диске. В PhoneJS входят классы для работы с данными: выборка, фильтрация, сортировка, группировка. Есть несколько вариантов хранения данных: OData и LocalStorage. Делать серверную часть для бесплатного приложения мне совершенно не хотелось, поэтому обратил внимание на LocalStorage. Довольно быстро выяснилось, что хранить в LocalStorage не самая здравая идея. Например, при обновлении на iOS 5.1 терлись пользовательские данные, другие жалуются, что LocalStorage трется раз в неделю на Android, третьи, что трется при выключении питания. Я решил не рисковать пользовательскими данными и использовать File API из Cordova/PhoneGap.
Документация утверждает, что это API основано на W3C File API. По факту это означает, что это API — разное в Chrome и Safari на Mac OS, реализации в Cordova для iOS и реализации для Android. Реализации для iOS и Android — разные. Функции себя ведут по разному и разный набор классов и констант. Например, в реализации для Android отсутствует класс Blob и константа window.PERMANENT. Зато есть класс LocalFileSystem с классом LocalFileSystem.PERSISTENT. В браузере на ноутбуке есть дополнительное API для запроса квоты на хранение данных, которое отсутствует на мобильных телефонах.
Дополнительных проблем создает доступная в сети документация по этому API. Я читал несколько статей, которые я смог найти по запросу html5 file api. Мне ни разу не встретилась статья, которая дала бы мне ответы на все мои вопросы. В итоге, методом проб и ошибок, родился класс для работы с File API, поддерживающий Cordova 3.3 на iOS и Android, Chrome 32 на Mac OS и Windows 8. Вы можете найти его здесь: https://github.com/chebum/fileStorage-for-Phone.JS/blob/master/fileStorage.js
Использовать его можно так:
// В этом примере создастся файл data/records в папке Documents приложения.
FS.initFileAPI(1000000, true)
.then(function () {
var records = new FS.FileArrayStore({ key: "id", fileName: "records" });
return records.insert({ customer: "Петя" })
})
.then(function () {
alert("Запись добавлена!");
});
// Или использовать низкоуровневое API:
FS.initFileAPI(100000, true)
.then(function() {
return FS.writeFile("file1", "Содержимое файла")
})
.then(function() {
alert("Файл записан!");
});
+33 минуты Сохранение введенных записей в сторедж. Список категорий вынесен в ArrayStore, чтобы упростить выборки.
+26 минут Собственный layout для вьюшек приложения. PhoneJS предлагает набор Layout, которые являются своего рода обрамлением для вью, которые они содержат. Поскольку в моем приложении главная страница не укладывалась ни в один из доступных layout'ов, я выбрал EmptyLayout. У него оказался минус: отсутствовала анимация переходов между экранами приложения. Я скопировал код EmptyLayout и вставил аттрибут, добавляющий анимацию при переключении экранов.
+1 час 51 минута Экран About из шаблона приложения переделан в экран отчетов, пока пустой. Сделана viewModel с выборкой данных текущего месяца. Добавлено локализованное форматированные даты для заголовка экрана.
+59 минут Группировка и вывод трат по группам в текущем месяце.
+28 минут Открытие списка выбора месяца для отчета по нажатию на заголовок экрана.
+1 час 20 минут Добавил Cordova-плугин Statusbar, который отказался работать «из коробки». Выяснилось, что в шаблоне приложения от PhoneJS по какой-то причине была закомментирована ссылка на cordova.js:
<!--<script type="text/javascript" src="cordova.js"></script>-->
В результате нативная часть приложения работала некорректно.
+39 минут На экране с отчетами верхняя часть заменена на dxToolbar.
+22 минут Выяснял почему не работает обработчик нажатия dxButton. Убирание FastClick.js решило проблему, но появилась ощутимая задержка при нажатии кнопок клавиатуры. Подписка на dxAction переделана на touchstart.
+25 минут Форматирование вывода строк в отчете (были косяки).
Всю ночь мне снились дрянные кнопки на главном экране приложения.
Место действия: Тула, Внуково, Москва, Дата: 7-8 января 2014
У меня был ранний рейс в Будапешт из Внуково, поэтому что не успел днем доделывал ночью в аэропорту. Спать, сидя на стуле в кафе неудобно, а программировать — вполне.
+2 часа 5 минут Утром, еще дома, решил разделить кнопки, чтобы избавиться от границ между ними. В качестве образца взял клавиатуру набора номера из iOS7.
Клавиатуры получилось три. В зависимости от габаритов экрана меняется размер кнопок: для 3.5'', 4'' и 5'' телефонов. В каждую ячейку таблицы был положен div и настроено выравнивание.
Из-за отсутствия недоделаного выравнивание текста по вертикали в HTML, получился достаточно сложный css-стиль для кнопок:
.home-view .buttons td div {
color: #4a5360;
border: 1px solid #4a5360;
text-align: center;
position: absolute;
left: 50%;
/* Small buttons - default */
font-size: 26px;
padding: 13px 0 13px 0;
width: 52px;
line-height: 26px;
border-radius: 26px;
margin-left: -27px;
margin-top: -27px;
}
+1 час 50 минут Купил на Fotolia набор векторных иконок. Вырезал нужные и конвертировал в PNG формат. Возможно, так долго получилось из-за того, что было пол второго ночи:)
+1 час 10 минут Сплеш-скрин загрузки приложения
+36 минут Иконка приложения в 3х размерах. Локализация названия для iOS.
+20 минут Скрывание сплеш-скрина после полной загрузки приложения.
+2 часа Фикс многочисленных багов
+2 часа Экраны для Play Store
+30 минут Экраны для App Store
+30 минут Описание приложения для 2х магазинов
+1 час 30 минут Сабмит в App Store — возникли непонятные проблемы с подписью приложения.
Моя Бухгалтерия
Сложим потраченное время и разобъем его по категориям.
Разработка: 21 час 37 минут
Графика и тексты: 8 часов 26 минут
Всего: 30 часов 3 минуты
В итоге за это время получилось минимально работающее приложение, которое сильно уступает последней версии «Транжиры». Я не успел разбиение трат по дням, ввод доходов. В интерфейсе программы срезано несколько «углов».
Если проанализировать их трудозатраты, то получается следующее. В своей статье они пишут про 3-4 дня 4 человек. Это 96-128 человеко-часов. У меня получилось чуть больше 30 часов и приложение под 3 платформы. iOS и Android уже в магазинах. Windows 8 потребует переделки интерфейса: текущий очень чужероден.
Можно гордиться собой.