В этом топике мы хотим поделиться нашим опытом создания мобильных приложений на платформе для разработки кроссплатформенных приложений Titanium. Примерно с 2011 мы начали работы с кроссплатформенными фреймворками. Сначала это был PhoneGap, потом Titanium. Сделали десяток приложений, работающих и по сей день, как в России, так и в США. Мы сознательно хотим отойти от оценок — плохо это или хорошо разрабатывать кроссплатформенные приложения, а сосредоточиться на тех трудностях, с которыми предстоит столкнуться с точки зрения разработки и сопровождения этих приложений.
На наш взгляд, топик будет полезен как читателям, которые собираются заказать приложение, чтобы они могли сделать выбор между native-разработкой на каждую платформу и кроссплатформенностью, так и разработчикам, которые принимают решение, куда идти.
Итак, начнем со списка проблем, с которыми вам придётся столкнуться.
Проблемы:
- Проблема дабл-кликов.
- If’ный код.
- Управление памятью на Android.
- Недостаточная реализация отдельных функций, в том числе стандартных.
- Javascript — отсутствие типизации замедляет процесс написания кода и усложняет сопровождение.
- Отсутствие InterfaceBuilder — замедляет процесс написания приложения, весь UI пишется в коде.
- Titanium SDK обновляется позже SDK операционных систем.
- Каждая версия SDK содержит исправление старых ошибок и привносит новые ошибки.
Примеры, на которых мы столкнулись с этими проблемами.
Проблема дабл-кликов
Мобильное приложение содержит различные элементы управления – кнопки, поля ввода текста, переключатели и т.п. Когда пользователь нажимает на какой-либо из них, приложение получает сигнал об этом в виде события – объекта, содержащего информацию о том, с каким элементом было произведено действие, какое это действие, например, долгое или короткое нажатие, и другое. В нативных приложениях, т.е. в приложениях, написанных с использованием стандартных средств разработки (iOS SDK для iPhone и Android SDK для Android), во время обработки события соответствующий элемент управления блокируется, и новое событие от него прийти не может. Наверное, все замечали, что если нажать на кнопку отправки сообщения в стандартном приложении, то на какое-то время кнопка становится серой и нажать на нее второй раз нельзя. Такая блокировка в нативных приложениях происходит автоматически и не требует от программиста написания кода или других действий. В приложении, написанном с использованием Titanium SDK, такой блокировки нет, поэтому каждый элемент управления может отправить несколько однотипных событий. Если, например, по нажатию на кнопку открывается новый экран приложения, то может открыться два или больше экранов.
Это неправильно и неудобно для пользователя. Apple, скорее всего, даже не пропустит такое приложение в AppStore. Приходится блокировать UI или использовать флажки для того, чтобы игнорировать последующие события до окончания обработки первого. Наши тестировщики назвали эту проблему проблемой дабл-кликов.
Мы придумали несколько способов решения этой проблемы:
1. В первом варианте мы сделали блокировку экрана: после первого нажатия весь экран приложения закрывается сообщением «Пожалуйста, подождите...» на полупрозрачном сером фоне. Для простых ситуаций сообщение скрывается через определенное время (около полсекунды). Для более долгих операций, требующих, например, загрузки данных, необходимо заблокировать экран на неопределенный срок, и тогда в конце обработки события вызывается специальная функции разблокировки экрана, которая скрывает сообщение. Само сообщение служит щитом для последующих действий пользователя.
Выглядело это примерно так:
2. В другом варианте выполняется проверка, что со времени последнего нажатия прошло не меньше заданного интервала времени. Если полсекунды с последнего нажатия прошло, то выполняется обработка события, иначе оно игнорируется.
В обоих случаях для пользователя это менее понятно, чем поведение нативного приложения, в котором кнопка просто не может нажаться в процессе обработки предыдущего события.
If’ный код
Многие вещи для разных операционных систем реализуются с помощью различных функций, свойств и модулей, в коде программы постоянно приходится писать «если это Android, то делаем так, а если iPhone, то вот так». Сначала заводится переменная, чтобы писать меньше кода, ее значение заполняется в начале работы приложения:
if(Titanium.Platform.name.indexOf('iPhone') >= 0) {
isIPhone = true;
isAndroid = false;
}
И вы начинаете везде вставлять проверки.
получение объектов из базы данных:
if(isAndroid) {
fieldCount = resultSet.fieldCount;
} else {
fieldCount = resultSet.fieldCount();
}
Это очень замечательный пример. Во-первых, по нему видно, сколько внимания нужно уделять мелочам, когда вы работаете с Titanium. Во-вторых, в документации сначала было написано неправильно, так что разработчикам пришлось посмотреть исходный код Titanium, чтобы исправить код своего приложения. Вообще, в документации Titanium обычно написано, для какой операционный системы работает тот или иной параметр:
Если под параметром нарисован Android, значит он работает в приложениях на Android, если нарисован iPhone или iPad, значит это параметр для iOS. И таких пометок в документации очень много.
Секции в таблицах
Если обновить данные в таблице с секциями на Android, то приложение упадет. Поскольку секции на Android не умеют вести себя так красиво, как на iOS, оставаясь в верхней части экрана при прокручивании списка под секцией, то мы вставляли представление секции в первый элемент списка для каждой секции. Тогда приложение не падает.
Отображение текста – лейблы, и поля ввода текста
Android делает отступы внутри текстовых полей, и эти отступы зависят от версии Android, от размера текстового поля и размера шрифта в нем. Обычная проблема – это невидимый текст, который вроде бы должен войти в текстовое поле, но не входит, из-за этих внутренних отступов. Если вы хотите, чтобы все выглядело одинаково хорошо, вам придется написать метод с большим количеством if'ов, который будет определять, как сейчас нужно отобразить текст.
var createLabel = function(properties, linesCount) {
var fontSize = isNotEmpty(properties.font) && (properties.font.fontSize) ? properties.font.fontSize : defaultFontSize;
var offset = Math.floor(fontSize/8);
var heightOffset = 2 * offset;
var lineHeight = fontSize + heightOffset;
var androidTopOffset = 0;
if (isAndroid) {
androidTopOffset = (fontSize <= getControlSize(18)) ? Math.floor(fontSize / 4) : Math.floor(fontSize / 11);
}
if(isNotEmpty(properties.top)) {
properties.top = properties.top - offset - androidTopOffset;
if(isNotEmpty(properties.height) && properties.height != Ti.UI.SIZE) {
properties.height = properties.height + heightOffset;
} else if(isNotEmpty(linesCount)) {
properties.height = lineHeight * linesCount;
}
}
if (isEmpty(properties.font)) {
properties.font = {};
}
properties.font.fontFamily = fontName;
properties.font.fontWeight = 'normal';
if (isEmpty(properties.wordWrap)) {
if (isNotEmpty(linesCount) && linesCount == 1) {
properties.wordWrap = false;
} else {
// properties.wordWrap = true;
}
}
// DEBUG
if (isBlank(properties.color)) {
properties.color = '#ff0000';
properties.font.fontWeight = 'bold';
Ti.API.error('Error in createCommonLabel: not specified label color. Label text = ' + properties.text);
}
var label = Ti.UI.createLabel(properties);
return label;
};
Отдельное внимание стоит уделить секции DEBUG: если у текста не задан цвет, мы ставим красный цвет. Это сделано, потому что значение цвета по умолчанию для iPhone и Android разные и необходимо писать цвет каждому лейблу всегда – если вы хотите, чтобы приложение выглядело одинаково на разных устройствах. Также необходимо посчитать размер каждого элемента, ведь экраны Android’ов бывают очень разные, и размер шрифта и элементов управления должен соответствовать размеру экрана. Из-за таких несоответствий между платформами приложение необходимо тестировать на всех устройствах и операционных системах, причем даже тогда, когда исправляется что-то для одной из платформ – предсказать, как исправление отразится на других устройствах, иногда невозможно.
Управления памятью на Android
Когда была написана уже большая часть приложения, мы столкнулись с очень серьезной проблемой при тестировании на Android: при активной работе в приложении оно падает через 10 минут. Оказалось, что для каждого экрана выделяется большой кусок памяти, который не освобождается, даже когда экран закрыт. Эта проблема так и не была полностью решена, последняя версия, на которой проверяли, 3.1.1. Для повышения стабильности работы приложения пришлось сократить оформление интерфейса, убрать все фоновые картинки, уменьшить количество элементов, переписать часть стандартных элементов управления.
Если в приложении необходима загрузка данных из интернета, то тут тоже может крыться проблема. Если приложению не хватает памяти, то при загрузке данных приложение упадет. В нативном приложении для Android разработчик может добавить обработку исключений, ошибок, возникающих в ходе работы приложения, можно сделать даже отображение сообщения о недостатке памяти пользователю (хотя предпочтительнее разрешить проблему каким-либо другим способом). Когда вы пишете на Titanium, этой возможности нет. При загрузке данных ошибка происходит глубоко внутри Titanium, и программными средствами обработать ее нельзя.
Недостаточная реализация отдельных функций, в том числе стандартных
Наверное, самое большое количество подобных проблем мы собрали при реализации возможности добавить фото в приложении на Android. Первая проблема, с которой мы столкнулись, это невозможность написать код для выхода из режима фотографирования. Т.е. если приложение должно показать экран фотографирования с парой кнопок «Сделать фото» и «Отмена», то по нажатию на кнопку отмены вы не можете ничего отменить. Вторая проблема – это невозможность узнать о том, что пользователь вышел из режима фотографирования нажатием кнопки «Назад» на телефоне. Т.е. если по выходу из режима фотографирования необходимо обновить интерфейс – вы не можете этого сделать.
Многие подобные недоделки и проблемы стоят в списке багов Титаниума в течение нескольких версий.
jira.appcelerator.org/browse/TIMOB-16182
jira.appcelerator.org/browse/TIMOB-16199
Общий список проблем:
jira.appcelerator.org/secure/IssueNavigator.jspa?
Javascript — отсутствие типизации, замедляет процесс написания кода и усложняет сопровождение
Большинство современных разработчиков мобильных приложений привыкли писать программы на высокоуровневых языках, таких как Java и Objective-C, однако на Titanium приходится писать на Javascript. Это привычнее для веб-разработчиков, но они не знают основ мобильной разработки, не сталкивались с ограничениями памяти и правилами создания интерфейсов мобильных приложений. Javascript является языком с динамической типизацией, это значит, что переменная связывается с типом в момент присваивания значения, а не в момент объявления переменной. Таким образом, в различных участках программы одна и та же переменная может принимать значения разных типов. Для программиста это означает, что он должен сам следить, чтобы программа работала с данными в соответствии с их типами, поскольку в противном случае приложение будет падать.
Отсутствие InterfaceBuilder — замедляет процесс написания приложения — весь UI пишется в коде
Многие мобильные приложения содержат списки: твиттер, лента новостей, письма в почтовым ящике, адресная книга, напоминания, результаты поиска – это все списки. Обычно экран со списком реализуется на таблице. Каждый элемент — это ячейка, у которой описан шаблон. Для приложения на iOS программист создает шаблон (или различные шаблоны — для сложных таблиц) в Interface Builder
И пишет код для отображения данных в этом шаблоне:
- (void)updateViews
{
if (!self.package)
return;
static NSDateFormatter* df = nil;
if (!df) {
df = [[NSDateFormatter alloc] init];
df.dateStyle = NSDateFormatterShortStyle;
df.timeStyle = NSDateFormatterNoStyle;
}
self.labelCity.text = self.package.city;
self.labelPackageId.text = self.package.formattedPackageId;
self.labelItemsCount.text = @(self.package.items.count).stringValue;
self.prizeTypeImageView.image = [self prizeTypeImage];
self.labelDispatchDate.text = [df stringFromDate:self.package.dispatchDate];
self.labelPeriod.text = [NSString stringWithFormat:@"%@ – %@",
[df stringFromDate:self.package.periodBegin],
[df stringFromDate:self.package.periodEnd]];
}
На Титаниуме нужно написать все в коде, это выглядит так:
var rowTemplate = function(item, index, callback) {
var row = Ti.UI.createTableViewRow({
height: rowHeight,
left: 0,
right: 0,
selectedBackgroundColor: '#11a2c5',
backgroundSelectedColor: '#11a2c5',
color: 'transparent',
className: classNameStr
});
var view = Ti.UI.createView ({
top: 0,
left: leftOffset,
right: rightOffset,
height: rowHeight,
color: 'transparent'
});
var backView = Ti.UI.createView ({
top: 0,
left: 0,
right: voucherWidth,
height: rowHeight,
color: 'transparent'
});
view.add(backView);
var imageView = Ti.UI.createImageView({
top: top,
left: 0,
width: getControlSize(47),
height: getControlSize(58),
image: getPicturePath('/images/orders/icon_oteli_mini')
});
backView.add(imageView);
var left = imageView.left + imageView.width + topOffset;
var nameLabel = createCommonLabel({
left: left,
top: top,
right: topOffset,
height: imageView.height,
font: {
fontSize: fontSize
},
color: '#000000',
text: itemName
}, 2);
backView.add(nameLabel);
top = imageView.top + imageView.height;
var placeLabel = createCommonLabel({
left: 0,
top: top,
right: 0,
height: height,
font: {
fontSize: fontSize
},
color: '#000000',
text: itemCity
});
backView.add(placeLabel);
top = placeLabel.top + placeLabel.height;
var dateLabel = createCommonLabel({
left: 0,
top: top,
right: 0,
height: height,
font: {
fontSize: fontSize
},
color: '#000000',
text: datesText
});
backView.add(dateLabel);
var voucherView = Ti.UI.createImageView({
width: voucherWidth,
top: topOffset,
right: 0,
height: getControlSize(48),
backgroundImage: getPicturePath(voucherImagePath),
visible: voucherActive
});
view.add(voucherView);
top = dateLabel.top + dateLabel.height;
var numberLabel = createCommonLabel({
left: 0,
top: top,
right: 0,
height: height,
text: orderNumber,
font: {
fontSize: fontSize
},
color: '#000000'
});
backView.add(numberLabel);
top = numberLabel.top + numberLabel.height;
var statusLabel = createCommonLabel({
left: 0,
top: top,
right: 0,
height: height,
font: {
fontSize: fontSize
},
color: '#000000',
text: statusText
});
backView.add(statusLabel);
return row;
};
В Titanium 3.1.0 появилась новая возможность реализовать список – ListView. На Android она работает существенно быстрее таблицы, и реализация списков на ListView позволяет сделать приложение, которое не будет тормозить и подвисать при прокручивании списка. Для этого нужно переписать создание ячейки в виде шаблона:
var listItemTemplate = {
properties: {
height: rowHeight,
backgroundSelectedColor: '#11a2c5'
},
childTemplates: [
{
type: 'Ti.UI.View',
bindId: 'rootView',
properties: {
left: leftOffset,
top: 0,
right: rightOffset,
backgroundSelectedColor: '#11a2c5',
height: rowHeight,
color: 'transparent'
},
childTemplates: [
{ // backView
type: 'Ti.UI.View',
bindId: 'backView',
properties: {
left: 0,
top: 0,
color: 'transparent',
backgroundSelectedColor: '#11a2c5',
right: voucherSize,
height: rowHeight
},
childTemplates: [
{
type: 'Ti.UI.ImageView',
bindId: 'icon',
properties: {
left: 0,
top: top,
width: getControlSize(47),
height: height,
image: getPicturePath('/images/icon_mini')
}
},
{
type: 'Ti.UI.Label',
bindId: 'name',
properties: {
left: left,
top: top,
right: top,
height: height,
font: {
fontSize: fontSize,
fontFamily: fontName
},
color: '#000000'
}
},
{
type: 'Ti.UI.Label',
bindId: 'country',
properties: {
left: 0,
top: top + height,
right: 0,
height: subHeight,
font: {
fontSize: fontSize,
fontFamily: fontName
},
color: '#000000'
}
},
{
type: 'Ti.UI.Label',
bindId: 'date',
properties: {
left: 0,
top: top + height + subHeight,
right: 0,
height: subHeight,
font: {
fontSize: fontSize,
fontFamily: fontName
},
color: '#000000'
}
},
{
type: 'Ti.UI.Label',
bindId: 'number',
properties: {
left: 0,
top: top + height + subHeight + subHeight,
right: 0,
height: subHeight,
font: {
fontSize: fontSize,
fontFamily: fontName
},
color: '#000000'
}
},
{
type: 'Ti.UI.Label',
bindId: 'status',
properties: {
left: 0,
top: top + height + subHeight + subHeight + subHeight,
right: 0,
height: subHeight,
font: {
fontSize: fontSize,
fontFamily: fontName
},
color: '#000000'
}
}
]
},
{
type: 'Ti.UI.ImageView',
bindId: 'icon',
properties: {
right: 0,
top: top,
width: voucherSize,
height: voucherSize,
image: getPicturePath('/images/icon_act')
}
}
]
}
]
};
Поскольку реализация на ListView полностью меняет код создания экрана приложения, то имеет смысл вынести создание этого экрана для iPhone и Android в различные файлы и добавить условия для подключения одного из них в соответствии с платформой, на которой запускается приложение.
Titanium SDK обновляется позже SDK операционных систем
Не так давно Apple выпустила iOS 7, где была существенно переработана графика, работа со статус баром (верхняя часть экрана, где отображается уровень заряда аккумулятора, время и оператор), и другое.
Как обычно происходит выпуск новой операционной системы на рынок? Сначала сторонним разработчикам, т.е. разработчикам мобильных приложений, предоставляется доступ к бета-версии нового SDK для создания приложений под новую операционную систему. Т.е. все желающие выпустить приложение, поддерживающее новейшие возможности системы, могут и должны подготовиться заранее. Потом появляется стабильная SDK и начинают приниматься для проверки в AppStore приложения с поддержкой новой версии iOS. Потом пользователям – владельцам iPhone’ов – становится доступно обновление операционной системы и успевших подготовиться приложений.
Что происходит с теми, кто разрабатывает приложение на Titanium? Первая бета-версия iOS SDK доступна разработчикам с 11 июня 2013, версия Titanium SDK с поддержкой беты iOS – c 15 августа (SDK 3.1.2). Приложения с поддержкой новой версии iOS принимались на проверку c 11 сентября 2013.
Операционная система была доступна пользователям с 18 сентября 2013 года: у разработчиков была неделя, чтобы пройти проверку Apple.
Titanium выпустил версию 3.1.3 RC 9 сентября, а стабильную версию 3.1.3 с поддержкой iOS 7 – только 18 сентября, т.е. когда момент быть первым на рынке был уже безвозвратно упущен.
В версии 3.1.2 не была реализована поддержка нормального поведения StatusBar’а. Во-первых, если приложение раньше работало в полноэкранном режиме, то теперь оно отступает от верхнего края экрана на размер статус бара, и содержимое экранов может оказаться расположено совершенно неожиданно, в зависимости от того, как задавались координаты элементов при создании приложения. Но даже если вы решили бы сделать приложение, работающее не в полноэкранном режиме, то статус бар все равно бы не работал как полагается – вместо статус бара отображается черная полоса без какой-либо информации: ни часов, ни заряда, ни оператора на нем не отображается. С выходом Titanium 3.1.3 ситуация заметно улучшилась, однако в режиме фотографирования появляется статус бар даже у полноэкранного приложения jira.appcelerator.org/browse/TIMOB-15203 — эту ошибку исправили только в версии Titanium 3.2.0, которая вышла 20 декабря.
Каждая версия SDK содержит исправление старых ошибок и привносит новые ошибки
На примере все той же версии 3.1.3: при переходе на нее с версии 3.1.2 значительно замедлилась анимации в приложении. Например, нам нужно, чтобы два элемента интерфейса одновременно плавно переместились слева направо. В нативных приложениях разработчик может описать все анимации, которые необходимо применить к элементам интерфейса и запустить анимации выполняться одновременно. В предыдущих версиях Titanium описание анимаций для отдельных элементов было последовательным, но запускалось почти одновременно, так что визуально воспринималось как одна анимация, действующая с несколькими объектами. В Titanium 3.1.3 расхождения по времени между последовательно запущенными анимациями стало очень заметным и может составлять 1-3 секунды. По завершению анимации обычно необходимо выполнить какие-то операции, изменить положение элементов (сама анимация их не меняет), удалить объекты, которые больше не видны, чтобы они не занимали память. В Titanium 3.1.3 вызов постобработки анимации случается не всегда. Хотите починить статус бар – обновите версию SDK. Да, у вас перестанут работать анимации. Ну и что. Обновитесь дальше. Можно просто никогда не сделать приложение, которое может пройти проверку Apple, ожидая, когда же починят баги Titanium. А можно его даже не собрать.
В нашей практике был случай, когда обновление Titanium Studio – среды разработки, которая используется для сборки приложений – привело к тому, что функция сборки приложения для публикации просто перестала работать: jira.appcelerator.org/browse/TC-1322 (SDK 2.1.3 RC2 will not build for iTunes store distirbution) или jira.appcelerator.org/browse/TC-2193 (Studio 3.1 Distribute for iTunes Store is producing ad-hoc builds) – при попытке собрать приложение для публикации в App Store собирается приложение для AdHoc распространения – в этом случае помогла переустановка студии.
В заключение хочется заметить, что кто-то может сказать, что все это ерунда и проблемы можно обойти. Вопрос только один — нужно ли это делать и нужно ли это делать за счет качества и удобства приложений, отставания от рынка, постоянно увеличивающихся расходов на сопровождение? Мы для себя решили, что оно того не стоит – специализированные задачи нужно решать созданными для этого инструментами.
Автор: eastbanctech