- PVSM.RU - https://www.pvsm.ru -

DXWorkout — мобильное HTML5-приложение на PhoneJS и ChartJS

В наших прошлых постах мы писали про PhoneJS [1], HTML5-фреймворк для мобильных приложений, и ChartJS [2], JavaScript библиотеку визуализации данных. Если вы читали эти посты, то наверняка помните TipCalculator [3], простейшее демо-приложение, на примере которого мы рассказывали, с чего начать работу с PhoneJS.

На этот раз мы решили показать процесс создания чего-то более сложного, претендующего на применение в реальной жизни и использующего возможности фреймворка намного глубже. Момент как нельзя подходящий, ведь мы только что выпустили очередное плановое обновление [4] наших продуктов.

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Многие сотрудники нашей компании регулярно посещают тренажерные залы, причем одни ведут лог результатов в бумажных блокнотах, а другие эти результаты вообще не записывают, полагаясь на свою память. Поэтому мы постарались решить сразу две задачи — написать демо-приложение, раскрывающее возможности PhoneJS [5] и ChartJS [6], и помочь нашим сотрудникам, создав для них удобный инструмент ведения лога тренировок.

Сохранить баланс между демкой и реальным приложением было непросто, но у нас получилось.
Так появился DXWorkout [7] — мобильный помощник при силовых тренировках. Он позволяет записывать прогресс во время посещения тренажерного зала, а также дает возможность оценить результаты на графиках и посмотреть данные о прошлых тренировках.

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

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Одной из проблем, с которой мы столкнулись при проработке UI, было то, что использование приложения должно быть максимально простым. Мы убрали весь необязательный функционал, который планировали включить изначально, и сосредоточились на реализации самых важных моментов. В итоге появилась вот такая «раскадровка» (нарисована в Balsamiq Mockups [8]):

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS [9]

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

Мы решили использовать популярную сейчас на мобильных устройствах боковую навигацию [10] (которую впервые использовал Facebook в своем приложении для iPad). Такой подход позволяет не занимать драгоценное место на экране, но меню всегда можно вызвать уже привычным жестом (свайп вправо). Боковая навигация уже реализована в PhoneJS в качестве одного из стандартных layout-ов [11] под названием SlideOut.

Структура приложения

После того как с пользовательским интерфейсом все стало ясно, мы занялись проработкой модели данных (напомним, что PhoneJS пропагандирует парадигму MVVM). Тренировка описывается целью, датой начала и продолжительностью. Она состоит из набора именованных упражнений, в процессе которых человек делает один или несколько подходов, повторяя упражнение несколько раз с определенным весом.

Сразу было очевидно, что этот набор данных должен быть связан с несколькими экранами. Поэтому мы вынесли общую часть вью-моделей и разделили её на 3 блока — сама тренировка, отдельное упражнение и подход. Код каждого блока помещен в свой отдельный файл в папке viewModels [12].

В предыдущей статье [1] мы рассказали о том, с чего начинается приложение. Напомним, что точкой входа является файл index.html [13], где заданы необходимые META-теги и подключены все остальные ресурсы, а в файле index.js [14] объявлено пространство имен DXWorkout и задана функция инициализации приложения, которая будет выполнена, когда DOM-дерево полностью загрузится. Также здесь происходит подписка на различные события, как специфичные для PhoneJS, так и связанные с взаимодействием с устройством, предоставляемые PhoneGap.

Остальные файлы проекта разнесены по соответствующим папкам:

  • css [15] — общие стили и стили для различных платформ из библиотеки PhoneJS
  • js [16] — скрипты фреймворка и сторонних библиотек, а также скрипты, описывающие data layer
  • layouts [17] — необходимые для работы приложения layout-ы, в данном случае SlideOut Layout
  • Resources [18] — иконки и сплеш-скрины
  • viewModels [12] — общая вьюмодель тренировки, используемая на нескольких экранах и поделенная на файлы согласно логической структуре, описанной выше
  • views [19] — разметка и код отдельных экранов

Приложение DXWorkout состоит из нескольких экранов, и при переходе между некоторыми из них необходимо передавать параметры. Конечно, всегда можно сохранить их в глобальную переменную sessionStorage, но правильнее было бы настроить маршрутизацию (routing [20]) и передавать параметры в адресной строке. Для DXWorkout задана маршрутизация, которая позволяет кроме названия экрана передавать ещё 2 параметра: action и item. При регистрации правила маршрутизации (метод register) в качестве второго параметра передается объект со значениями по умолчанию:

app.router.register(":view/:action/:item", { view: "Home", action: undefined, item: undefined });

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

app.navigate();

Экраны

Поговорим немного о различных экранах приложения и их code-файлах. Мы соблюдаем принципы декомпозиции приложения и храним разметку и код для каждого экрана в отдельном файле (см. папку views [19]). В разметке к корневому контейнеру привязывается объект dxView c определенным именем:

<div data-options="dxView : { name: 'List' } " >
  ...
</div>

В code-файле функция с таким же именем добавляется к пространству имен приложения:

DXWorkout.List = function (params) {
    var action = params.action,
        key = params.item,
        ...
};

Обратите внимание, что эта функция принимает аргумент-объект, содержащий параметры action и item, указанные в маршруте, и возвращает вью-модель, которая будет связана с разметкой.

Чтобы осуществить переход между экранами и передать параметры, нужно вызвать метод app.navigate, указав ему URL в формате, соответствующем зарегистрированным правилам роутинга:

DXWorkout.app.navigate('List/edit/goal'); // action = "edit", key = "goal"

Давайте рассмотрим основную функциональность и интересные особенности каждого экрана.

Home

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Код: HTML [21], Javascript [22]

После запуска приложения пользователь попадет на экран Home. На нем содержится статистика об интенсивности тренировок за последний месяц. Часть этих данных представлена с помощью графика. Особенностью диаграмм ChartJS [6] является возможность гибкого управления результатом. Например, можно легко настроить значения на координатных осях:

var chartOptions = {
        ...
        valueAxis: {
            ...
            label: {
                customizeText: function(data) {
                    return wo.formatTime(data.value);
                }
            }
        },
        ...
};

Также с этого экрана доступен функционал запуска новой тренировки. Кнопка “Start new workout” представлена виджетом dxButton [23], стили которого позволяют выглядеть ей по-разному на различных платформах. При этом результатом рендеринга всех виджетов PhoneJS является обычная html-разметка, что позволяет применять к ней дополнительные стили. Так в приложении DXWorkout мы увеличили размер всех кнопок, используемых при записи процесса тренировки, через CSS:

.actions .dx-button {
    margin: 10px 0;
    padding: 0;
    width: 100%;
    height: 40px;
}

List

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Код: HTML [24], Javascript [25]

Этот экран служит для работы со списками целей или упражнений. Он может быть использован либо для их выбора при записи процесса тренировки, либо для редактирования из настроек приложения. Для отображения данных в виде списка используется виджет dxList [26]. Фреймворк предоставляет возможность указывать шаблон для элемента списка:

<div data-bind="dxList: { items: keySettings, itemClickAction: handleItemClick }">
    <div data-options="dxTemplate : { name: 'item' }">
        <div class="list-item-title">
            <span data-bind="text: $data"></span>
        </div>
        <div data-bind="if: $root.canDelete">
            <div data-bind="dxButton: { text: 'Delete', clickAction: $root.handleDeleteClick }"></div>
        </div>
    </div>
</div>

Вы можете заметить, что в темплейте используются значения $data и $root. Это контекстные свойства [27], характерные для KnockoutJS [28]. $data ссылается на текущий контекст (что-то вроде this в Javascript), а $root указывает на вью-модель экрана. Также один из контейнеров темплейта будет отрендерен только в случае редактирования списка (когда поле canDelete во вьюмодели будет иметь значение true). Для этого применяется управляющая привязка (control-flow binding) if [29].

Exercise

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Код: HTML [30], Javascript [31]

Этот экран служит для заполнения данных о выполнении упражнения. Для ввода веса и количества повторов используется виджет dxNumberBox [32]. На этом экране мы используем управляющую привязку foreach для отображения набора однотипных элементов — подходов в упражнении:

<div class="content edit-sets" data-bind="foreach: exercise().sets">
    <div>
        <div class="dx-field-label">Set <span data-bind="text: ($index() + 1)"></span></div>
        <div class="dx-field-value">
            <div data-bind="dxNumberBox: { value: reps, min: 1, placeholder: 'Reps' } "></div>
            <span>×</span>
            <div data-bind="dxNumberBox: { value: weight, min: 0, placeholder: 'Weight' } "></div>
            <span data-bind="text: weightUnit"></span>
            <!-- ko if: canDelete() -->
            <div class="minus" data-bind="dxAction: handleDelete" />
            <!-- /ko -->
        </div>
    </div>
</div>

Обратите внимание, что управляющую привязку можно использовать как в атрибуте data-bind html-элемента, так и без контейнера на основе html-комментариев.

Высота содержимого экрана меняется при добавлении очередного подхода, поэтому нужно иметь возможность прокручивать контент на экране, когда его станет слишком много. Для этого предназначен контейнер dxScrollView [33]. Весь прокручиваемый контент должен быть помещен внутрь него:

<div data-bind="dxScrollView: {}">
    ...
</div>

Results

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Код: HTML [34], Javascript [35]

Этот экран показывает весь прогресс текущей тренировки. Кроме уже рассмотренных виджетов, здесь используется dxTextArea [36], предназначенный для ввода многострочного текста — заметок к тренировке.

Log

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Код: HTML [37], Javascript [38]

Здесь показан список выполненных тренировок, отсортированный по дате и сгруппированный по месяцам. Для того, чтобы отобразить сгруппированный список, необходимо виджету dxList [26] передать в опциях значение параметра grouped, равное true:

<div data-bind="dxList: { dataSource: log, grouped: true, itemClickAction: handleItemClick }">
    ...
</div>

GoalGraphs и WeightGraphs

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS

Код GoalGraphs: HTML [39], Javascript [40]

Код WeightGraphs: HTML [41], Javascript [42]

Эти экраны используются для отображения данных о тренировках в виде диаграмм ChartJS. На первом графике изображена круговая диаграмма dxPieChart [43], показывающая пропорции числа тренировок с различными целями за последний месяц. Второй график (dxChart [44]) содержит изменения максимального веса по упражнению с течением времени. Для перехода между графиками используется виджет dxTabs [45].

Settings

Код: HTML [46], Javascript [47]

Здесь пользователь может изменить единицы измерения веса и расстояния. Также отсюда осуществляется переход на редактирование целей тренировок или названий упражнений.

Data access layer

Работа с данными в DXWokrout вынесена в отдельную абстракцию. Благодаря этому легко изменить способ хранения или синхронизации хранилища и данных в памяти, просто заменив Data access layer и не затрагивая остальной код приложения. Сейчас в качестве хранилища выбран HTML5 Web Storage, который поддерживается абсолютным большинством современных браузеров [48]. В будущем мы планируем добавить реализацию облачного хранилища в DXWorkout.

На данный момент единственным недостатком Web Storage является то, что он умеет хранить только строковые пары ключ-значение. Однако это не является проблемой в нашем случае, так как все необходимые для хранения данные можно представить в виде JSON и легко сконвертировать в строку и обратно:

localStorage.setItem(CURRENT_KEY, JSON.stringify(workout));
// ...
var storageData = localStorage.getItem(CURRENT_KEY),
    workout;
if(storageData)
    workout = JSON.parse(storageData);

Весь кода слоя данных вынесен в отдельный файл userDataLocal.js [49].

Также в приложение включены тестовые данные, чтобы при запуске DXWorkout сразу можно было видеть, какую функциональность он предоставляет. Они находятся в файле sampleData.js [50].

Пара слов о PhoneGap

В DXWorkout мы работаем с несколькими событиями взаимодействия с платформой из Cordova Device API [51]:

  • deviceready — сигнализирует о том, что Cordova загружена и готова к работе;
  • backbutton — возникает при нажатии аппаратной кнопки Back (актуально для Android и Windows Phone устройств);
  • pause — возникает, когда ОС перемещает приложение в фоновый режим. Обычно это происходит при переключении между приложениями или, например, при входящем звонке.

Однако реализация Phonegap имеет следующие ограничения:

  • при подписке на событие backbutton нельзя инициировать его всплытие, чтобы ОС совершила действия по умолчанию (например, закрыла приложение);
  • обработчики события pause на iOS будут выполнены только после того, как приложение вновь станет активным.

Для того, чтобы решить проблему с backbutton, мы проверяем, есть ли для конкретного экрана зарегистрированные обработчики для кнопки Back, и, если таких не обнаружено, вызываем метод выхода из приложения, также доступный в Phonegap:

navigator.app.exitApp();

Однако это API работает только на устройствах Android. Поэтому мы расширили шаблон CordovaWP8App, добавив в него способ закрыть приложение Windows Phone 8 при получении определенной команды. Код шаблона также доступен на GitHub [52]. В приложении необходимо вызвать следующий код:

window.external.Notify("DevExpress.ExitApp");

При сборке установочного пакета с помощью нашего расширения DevExtreme [53] для Visual Studio никаких дополнительных действий, кроме написания этой строчки кода, не требуется.

iOS 7

Кстати, если говорить о поддержке нами различных операционных систем, то недавно [54] в PhoneJS была добавлена тема для находящейся в бете iOS7:

DXWorkout — мобильное HTML5 приложение на PhoneJS и ChartJS
До момента финального релиза новой версии iOS поддержку этой темы в приложении нужно включать вручную:

// enable iOS7 theme
var device = DevExpress.devices.current(),
    iosVersion = DevExpress.devices.iosVersion();
if(device.platform === "ios" && iosVersion && iosVersion[0] === 7) {
    $(".dx-viewport")
        .removeClass("dx-theme-ios")
        .addClass("dx-theme-ios7");
}

Публикация приложения

Кроме выкладывания кода на GitHub [55] и включения его в дистрибутив PhoneJS [56] мы также поставили цель опубликовать приложение в магазинах трех основных мобильных платформ. Это оказалось даже легче, чем мы думали! Так, Apple утвердил DXWorkout уже на третий день после загрузки пакета. Вот ссылки на AppStore, Google Play и Windows Phone Store:

Автор: amartynov

Источник [57]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/41220

Ссылки в тексте:

[1] PhoneJS: http://habrahabr.ru/company/devexpress/blog/182134/

[2] ChartJS: http://habrahabr.ru/company/devexpress/blog/185210/

[3] TipCalculator: https://github.com/DevExpress/PhoneJS-TipCalculator

[4] очередное плановое обновление: http://www.devexpress.com/Support/WhatsNew/DXTREME/files/13.1.6.xml

[5] PhoneJS: http://phonejs.devexpress.com/

[6] ChartJS: http://chartjs.devexpress.com/

[7] DXWorkout: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr

[8] Balsamiq Mockups: http://balsamiq.com/products/mockups/

[9] Image: http://habrastorage.org/storage2/be8/d57/57a/be8d5757a00552bea89909ca2a7348a7.jpg

[10] боковую навигацию: http://kenyarmosh.com/ios-pattern-slide-out-navigation/

[11] стандартных layout-ов: http://phonejs.devexpress.com/Documentation/Howto/Views_and_Layouts#Built-in_Layouts

[12] папке viewModels: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr/viewModels

[13] index.html: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/index.html

[14] index.js: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/index.js

[15] css: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr/css

[16] js: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr/js

[17] layouts: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr/layouts

[18] Resources: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr/Resources

[19] views: https://github.com/DevExpress/PhoneJS-DXWorkout/tree/habr/views

[20] routing: http://phonejs.devexpress.com/Documentation/Howto/Navigation_and_Routing

[21] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Home.html

[22] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Home.js

[23] виджетом dxButton: http://phonejs.devexpress.com/Documentation/ApiReference/Widget_Library/dxButton

[24] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/List.html

[25] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/List.js

[26] виджет dxList: http://phonejs.devexpress.com/Documentation/ApiReference/Widget_Library/dxList

[27] контекстные свойства: http://knockoutjs.com/documentation/binding-context.html

[28] KnockoutJS: http://knockoutjs.com

[29] if: http://knockoutjs.com/documentation/if-binding.html

[30] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Exercise.html

[31] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Exercise.js

[32] виджет dxNumberBox: http://phonejs.devexpress.com/Documentation/ApiReference/Widget_Library/dxNumberBox

[33] контейнер dxScrollView: http://phonejs.devexpress.com/Documentation/ApiReference/Widget_Library/dxScrollView

[34] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Results.html

[35] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Results.js

[36] dxTextArea: http://phonejs.devexpress.com/Documentation/ApiReference/Widget_Library/dxTextArea

[37] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Log.html

[38] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Log.js

[39] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/GoalGraphs.html

[40] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/GoalGraphs.js

[41] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/WeightGraphs.html

[42] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/WeightGraphs.js

[43] круговая диаграмма dxPieChart: http://chartjs.devexpress.com/Documentation/ApiReference/dxPieChart

[44] dxChart: http://chartjs.devexpress.com/Documentation/ApiReference/dxChart

[45] виджет dxTabs: http://phonejs.devexpress.com/Documentation/ApiReference/Widget_Library/dxTabs

[46] HTML: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Settings.html

[47] Javascript: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/views/Settings.js

[48] абсолютным большинством современных браузеров: http://caniuse.com/#feat=namevalue-storage

[49] userDataLocal.js: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/js/userDataLocal.js

[50] sampleData.js: https://github.com/DevExpress/PhoneJS-DXWorkout/blob/habr/js/sampleData.js

[51] Cordova Device API: http://cordova.apache.org/docs/en/2.9.0/cordova_device_device.md.html#Device

[52] на GitHub: https://github.com/DevExpress/DevExtreme-ApplicationTemplate-WP8

[53] DevExtreme: http://www.devexpress.com/Products/HTML-JS/

[54] недавно: http://phonejs.devexpress.com/Blog/ios7-theme-beta-is-released

[55] GitHub: https://github.com/DevExpress/PhoneJS-DXWorkout

[56] дистрибутив PhoneJS: http://phonejs.devexpress.com/Download

[57] Источник: http://habrahabr.ru/post/190182/