Постановка задачи
Необходимо придумать и подготовить базу для быстрого создания типовых приложений кассы. Каждый кинотеатр хочет отдельные приложения, приложение постоянно развивается, правятся баги и добавляются новые фичи, которые так же необходимо подтягивать в брендированные версии.
Цели и конверсия
Кастомизированные приложения положительно влияют на конверсию. Основная цель пользователей в приложении – просмотр расписания любимого кинотеатра и покупка билетов. Наличие приложений позволяет сократить кол-во кликов от открытия до нажатия кнопки купить билет на 2-3 клика. А как известно, каждый клик это -N% пользователей. Так же присутствует возможность доносить до пользователей информацию об акциях и скидках.
Архитектура
Изменения касаются UI, основные сущности и функционал остаются без изменений. Фильтрация источников не требуется, выполняется со стороны API.
Изначально была задача сделать одну брендированную версию, планы по развитию были весьма туманны. Ветвление по версиям было через if. Примерно так:
if (TYPE == KASSA1) {
return 1;
} else if (TYPE == KASSA2) {
return 2;
} else {
return 3;
}
Или через switch:
switch (TYPE) {
case KASSA1:
return 1;
case KASSA2:
return 2;
default:
return 3;
}
Решение очень плохое, множество недостатков и почти нет преимуществ. Вся разработка в одной кодовой базе, огромное количество кода, сложная отладка и дебаг, проблема деплоя. Вся гамма эмоций на одной картинке:
Flavors
Следующей веткой эволюции стало использование Gradle Flavors + build Types
Создаются отдельные версии приложений, переключение между ними происходит в окошке build types, параллельно можно задавать конфигурации, например: debug, release, manager, testOrder и так далее. Каждая конфигурация содержит свой код и ресурсы. Одной из неприятных особенностей flavors является сложная навигация по коду в неактивной ветке. Пример конфигурации:
productFlavors {
kassaCinema1 {
applicationId "ru.rambler.cinema1"
versionName "1.3"
}
kassaCinema2 {
applicationId "ru.rambler.cinema2"
versionName "1.1"
}
}
Основные возможности:
- ? Отдельные ресурсы
- ? Отдельный код
- ? Легкая сборка
Недостатки:
- ?Отсутствие возможности проверки корректности других веток
- Сложность навигации при большом количестве веток.
Более подробно можно почитать: tools.android.com/tech-docs/new-build-system/user-guide и тут: developer.android.com/tools/building/configuring-gradle.html
Для большинства приложений данного функционала должно хватать, однако из-за возможности активной работы только в одной ветке, а также неудобства при большом количестве версий, пришлось отказаться от этой системы в пользу SDK или android library.
«SDK или приложение – библиотека»
Следующим шагом, стал перенос основного кода в SDK. Основное приложение создается как библиотека, т.е. в build gradle меняем:
apply plugin: 'com.android.application'
на
apply plugin: 'com.android.library'
и удаляем все лишнее из манифеста.
Также создается основная реализация, в ней описывается манифест и подключается библиотека, свой код или ресурсы в основной версии не используются.
Вся логика и основной код находится в библиотеке, в брендированных версиях содержится только дизайн, в редких случаях, отельные блоки кода.
Реализация
Вся логика брендированных приложений находится в главной фабрике. Именно она отвечает за тонкую настройку всего приложения. Инициализация в Application.
public class KassaApp extends Application {
private MainFactory mainFactory;
@Override
public void onCreate() {
super.onCreate();
mainFactory = createMainFactory()
}
protected MainFactory createMainFactory() {
return new MainFactory();
}
}
Пример Application в кастомном приложении:
public class CustomApp extends KassaApp {
@Override
protected MainFactory createMainFactory() {
return new CustomFactory();
}
}
Пример фабрики основных сущностей:
public class MainFactory {
public FragmentManager createFragmentManager() {
return new FragmentManager();
}
public UIManager createUIManager() {
return new UIManager();
}
public String getLatLng() {
return LocationSettingsManager.getInstance().getLatLngParams();
}
public String getCustomUrl() {
return getString(R.string.custom_url);
}
public Place getCustomPlace() {
throw new IllegalStateException(getString(R.string.error_illegal))
}
}
Изменение view посредством менеджеров
public class UIManager {
public boolean hasHeaderLocation() {
return getBoolean(R.bool.config_header_location_enabled);
}
public boolean hasPosterSearch() {
return getBoolean(R.bool.config_poster_search_enabled);
}
}
Пример использования:
if (!uiManager.hasHeaderLocation()) {
disableHeaderLocationView();
}
За работу с фрагментами отвечает FragmentManager
public class FragmentManager {
public Fragment getOneCinemaFragment() {
throw new IllegalStateException(getString(R.string.error_illegal));
}
public Fragment getNavNowFragment() {
return new NavNowFragment();
}
public Fragment getNavSupportFragment() {
return new NavSupportFragment();
}
}
При добавлении элементов характерных для всех типовых кинотеатров, мы добавляем их в основной проект
При добавлении нетипичных элементов, мы используем подмену фрагментов
WebView
В случае, если заказчик хочет иметь возможность динамически изменять информацию в приложении, мы добавляем webView с вшитыми ссылками. За работу с webView отвечает отдельный фрагмент, просто подменяем ссылку.
<string name="custom_url_news">http://cinema1.ru/news/</string>
Контент и оформление страницы остается за заказчиком.
Дизайн
Дизайн в данном процессе один из самых трудоемких моментов, по договоренности дизайн отрисовывается по примеру основной версии, один-в-один. Все элементы поставляются в таком же формате, с такими же именами файлов, таким же расположением элементов. В итоге необходимо просто скопировать каталог res-drawable. Такая же ситуация с цветами и шрифтами.
<resources>
<color name="kassa_main_color">#1aa0f0</color>
<color name="kassa_delimiter">#bbe2f9</color>
<color name="kassa_clickable">#E3E3E3</color>
<color name="action_color_active">#1aa0f0</color>
<color name="action_color_pressed">#bbe2f9</color>
</resources>
Вся информация относительно кинотеатра хранится в ресурсах:
<string name="support_custom_email ">cinema1@cinema1.ru</string>
<string name="app_name">Cinema 1</string>
<string name="support_custom_phone">+7 495 777 77 77</string>
Аналогично хранится информация о видимости элементов, дизайне и прочем.
Мастер создания приложений.
Как видно, все настройки для отдельного кинотеатра (при отсутствии отдельных фрагментов), выносятся в resource файлы. Дизайн также подменяется на уровне картинок и ресурсов.
Это позволяет создавать приложения для отдельных кинотеатров с помощью мастер (обычное приложение) который возьмет образец проекта, перезапишет настройки ключей, цвета, названия и прочую информацию, положит в нужную директорию и добавит в общий проект. Нам останется только скомпилировать проект и выложит в гугл плей (хотя даже и это можно автоматизировать).
Тестирование
Для автотестов используется Robolectric, для UI — espresso.
Для тестирования используется fabric.io (ex Crashlytics). Для интеграции в приложение используется плагин для android Studio. Заливка на сервер с помощью команды gradle. Настройки для тестирования:
ext.betaDistributionReleaseNotes=”Release Notes for this build.”
ext.betaDistributionEmails="BetaUser@y.com, BetaUser2@y.com"
ext.betaDistributionGroupAliases=”my-best-testers”
Тесты для приложений выполняются автоматически на Jenkins, при успешном прохождении тестов, сборка автоматически уходит в fabric. Более подробно про инструментарий тестирования, можно почитать тут:
habrahabr.ru/company/rambler-co/blog/266837
Доступен анализ покрытия кода тестами и успешности выполнения
Публикация на Google play
При большом количестве приложений, публикация занимает достаточно продолжительное время. В данный момент приложения публикуются вручную, но рассматривается возможность использования Publishing API developers.google.com/android-publisher. По заявлениям разработчиков, это апи позволяет:
- ?Загружать новые версии
- Изменять статус приложения (альфа, бета)
- ?Изменять информацию о приложении
Заключение
В данной статье мы рассказали о развитии архитектуры Rambler кассы, от if-ов до полноценного мастера создания приложений. Выделение основного кода в библиотеку или sdk, использование мастера, внесение изменений с помощью ресурсов и адаптация дизайна позволяют упростить создание брендированных приложений.
Спасибо за внимание!
Комментарии (17)
rogrom
26.10.2015 20:30Было бы интересно увидеть именно деплой-скрипт, как вариант — в виде плагина для Gradle
VitaZheltyakov
26.10.2015 23:40-5Хотите быстро, легко и универсально, то используйте связку HTML5 + СocoonJS (или Cordova)
BIanF
28.10.2015 06:27Вы статью-то читали?
VitaZheltyakov
28.10.2015 11:02Я статью прочел внимательно. А вот люди, поставившие мне минус, явно не знают технологий, которые я перечислил
agent10
28.10.2015 12:38Статья об архитектурных подходах кастомизации/брендирования нативно написанных приложений с кучей кода и кучей заказчиков… Как технологии, которые вы перечислили связаны с этим?
VitaZheltyakov
28.10.2015 12:44Эти технологии позволяют создавать нативные приложения на основе web-приложений. Преимущества очевидны: простота и скорость разработки, больший охват платформ.
akimovpro
27.10.2015 11:11Недавно Fastlane запилили под Android (пока бета) — github.com/fastlane/fastlane/blob/master/docs/Android.md
Под iOS отличный продукт для быстрого деплоя.
KamiSempai
27.10.2015 19:19Зря вы класс назвали как «FragmentManager» может получиться путаница с классом «android.app.FragmentManager».
BIanF
28.10.2015 06:29-1Читаю статьи такие и всегда думаю какие же крутые бывают спецы =)
У меня тоже схожая задача, но благо приложения (>100) отличаются только ресурсами. Есть приложение Template, которое редактируется через IDE, как и любое другое + Python скрипт, который подменяет ресурсы, файлы подписи и билдит.
ookami_kb
Была (да и есть) похожая задача по разработке и поддержке ~15 приложений, отличающихся содержанием + немного оформлением. В итоге всё-таки пришли к flavors. Если все версии всё равно в одном проекте — то смысла особого в выделении библиотеки как-то не вижу. Просто весь общий код выносится в main, а конкретные приложения разносятся по flavors.
На Publishing API давно засматриваюсь, но всё надеюсь, что запилят поддержку в Android Studio — было бы логично и удобно.
agent10
Скажите, а не усложняется ли навигация по проекту, когда весь общий код в main?
В нашем проекте ~7 приложений, исторически весь общий код как раз вынесен в отдельный library project, тогда как основной flavor main «пустует».
Пока переносить я не решаюсь, сейчас визуально работать проще(как мне кажется)…
ookami_kb
Т.е. прям лежит в отдельном проекте, там компилируется, и подключается к приложениям через jar/aar? Я просто так одну библиотеку свою тяну, которая в нескольких проектах используется, и каждый раз при изменении чего-нибудь в ней приходится кучу шагов предпринимать… так что мне кажется это сильно неудобно.
А особых неудобств main не доставляет — лежит себе спокойно; по сути, в отдельной папке всё инкапсулировано; зато, если надо что-то подправить, всегда под рукой.
agent10
Да, прямо так и лежит. Но подключается зависимостями через gradle. Но у нас, 90% работы именно с общим кодом. А как думаете ускорится ли сборка при переносе в main?
ookami_kb
Мне кажется, не особо. Не думаю, что накладные расходы на разрешение зависимостей будут как-то сильно влиять. Хотя было бы, пожалуй, познавательно узнать результат сравнения.
agent10
Попробовал ради интереса перенести, прирост всё же есть, но не очень значительный: на моей машине быстрее стало строиться примерно на 9%. До переноса среднее время было 112 секунд, после стало 102 секунды.
kolipass
Поддерживаю мнение о правильности реализации через gradle. Само собой договариваемся, что ядро лежит в maven репозитории, пусть даже и в приватном (хотя бы локальном).
Как факт — можно вообще отказаться от использования flavor и получить полностью независимые от соседей проекты. А ведь рано или поздно кто-то из заказчиков захочет себе много штук, которые будут нужны только ему: какая-нибудь интеграция кинофестиваля в календарь или реалтайм пушилка сообщений о лауреатах Оскара(с предметной облостью не знаком, думаю, имеются живые примеры). Или вообще захочет забрать исходники и свалить к другим. В таком случае вы отдадите только кастом код+ ссылку на зависимость из gradle репозитория.