Многие разработчики сталкиваются с задачей создания кастомизированных приложений. Например, разработка нескольких версий одного приложения или изменения стандартного приложения под требования заказчика. Мы в Rambler&Co столкнулись с такой задачей при разработке Rambler кассы и ее брендированных версий под отдельные кинотеатры. В данной статье рассмотрим эволюцию архитектуры такого приложения, а также инструменты, которые упрощают нам жизнь.



Постановка задачи


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

Цели и конверсия

Кастомизированные приложения положительно влияют на конверсию. Основная цель пользователей в приложении – просмотр расписания любимого кинотеатра и покупка билетов. Наличие приложений позволяет сократить кол-во кликов от открытия до нажатия кнопки купить билет на 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;
        }

Решение очень плохое, множество недостатков и почти нет преимуществ. Вся разработка в одной кодовой базе, огромное количество кода, сложная отладка и дебаг, проблема деплоя. Вся гамма эмоций на одной картинке:
IF, IF EVEREWHERE


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>

Контент и оформление страницы остается за заказчиком.

Пример меню с 4 элементами – webView (кинобар, реклама, аренда залов, акции).



Дизайн


Дизайн в данном процессе один из самых трудоемких моментов, по договоренности дизайн отрисовывается по примеру основной версии, один-в-один. Все элементы поставляются в таком же формате, с такими же именами файлов, таким же расположением элементов. В итоге необходимо просто скопировать каталог 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)


  1. ookami_kb
    26.10.2015 19:51

    Была (да и есть) похожая задача по разработке и поддержке ~15 приложений, отличающихся содержанием + немного оформлением. В итоге всё-таки пришли к flavors. Если все версии всё равно в одном проекте — то смысла особого в выделении библиотеки как-то не вижу. Просто весь общий код выносится в main, а конкретные приложения разносятся по flavors.

    На Publishing API давно засматриваюсь, но всё надеюсь, что запилят поддержку в Android Studio — было бы логично и удобно.


    1. agent10
      26.10.2015 22:27

      Скажите, а не усложняется ли навигация по проекту, когда весь общий код в main?
      В нашем проекте ~7 приложений, исторически весь общий код как раз вынесен в отдельный library project, тогда как основной flavor main «пустует».
      Пока переносить я не решаюсь, сейчас визуально работать проще(как мне кажется)…


      1. ookami_kb
        26.10.2015 22:39

        общий код как раз вынесен в отдельный library project

        Т.е. прям лежит в отдельном проекте, там компилируется, и подключается к приложениям через jar/aar? Я просто так одну библиотеку свою тяну, которая в нескольких проектах используется, и каждый раз при изменении чего-нибудь в ней приходится кучу шагов предпринимать… так что мне кажется это сильно неудобно.

        А особых неудобств main не доставляет — лежит себе спокойно; по сути, в отдельной папке всё инкапсулировано; зато, если надо что-то подправить, всегда под рукой.


        1. agent10
          26.10.2015 22:49

          Да, прямо так и лежит. Но подключается зависимостями через gradle. Но у нас, 90% работы именно с общим кодом. А как думаете ускорится ли сборка при переносе в main?


          1. ookami_kb
            26.10.2015 23:01

            Мне кажется, не особо. Не думаю, что накладные расходы на разрешение зависимостей будут как-то сильно влиять. Хотя было бы, пожалуй, познавательно узнать результат сравнения.


            1. agent10
              28.10.2015 21:02
              +1

              Попробовал ради интереса перенести, прирост всё же есть, но не очень значительный: на моей машине быстрее стало строиться примерно на 9%. До переноса среднее время было 112 секунд, после стало 102 секунды.


          1. kolipass
            27.10.2015 09:14

            Поддерживаю мнение о правильности реализации через gradle. Само собой договариваемся, что ядро лежит в maven репозитории, пусть даже и в приватном (хотя бы локальном).
            Как факт — можно вообще отказаться от использования flavor и получить полностью независимые от соседей проекты. А ведь рано или поздно кто-то из заказчиков захочет себе много штук, которые будут нужны только ему: какая-нибудь интеграция кинофестиваля в календарь или реалтайм пушилка сообщений о лауреатах Оскара(с предметной облостью не знаком, думаю, имеются живые примеры). Или вообще захочет забрать исходники и свалить к другим. В таком случае вы отдадите только кастом код+ ссылку на зависимость из gradle репозитория.


  1. rogrom
    26.10.2015 20:30

    Было бы интересно увидеть именно деплой-скрипт, как вариант — в виде плагина для Gradle


    1. anonymouz
      26.10.2015 20:45
      +1

      Вот эта штука работает: github.com/Triple-T/gradle-play-publisher


  1. VitaZheltyakov
    26.10.2015 23:40
    -5

    Хотите быстро, легко и универсально, то используйте связку HTML5 + СocoonJS (или Cordova)


    1. BIanF
      28.10.2015 06:27

      Вы статью-то читали?


      1. VitaZheltyakov
        28.10.2015 11:02

        Я статью прочел внимательно. А вот люди, поставившие мне минус, явно не знают технологий, которые я перечислил


        1. agent10
          28.10.2015 12:38

          Статья об архитектурных подходах кастомизации/брендирования нативно написанных приложений с кучей кода и кучей заказчиков… Как технологии, которые вы перечислили связаны с этим?


          1. VitaZheltyakov
            28.10.2015 12:44

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


  1. akimovpro
    27.10.2015 11:11

    Недавно Fastlane запилили под Android (пока бета) — github.com/fastlane/fastlane/blob/master/docs/Android.md
    Под iOS отличный продукт для быстрого деплоя.


  1. KamiSempai
    27.10.2015 19:19

    Зря вы класс назвали как «FragmentManager» может получиться путаница с классом «android.app.FragmentManager».


  1. BIanF
    28.10.2015 06:29
    -1

    Читаю статьи такие и всегда думаю какие же крутые бывают спецы =)
    У меня тоже схожая задача, но благо приложения (>100) отличаются только ресурсами. Есть приложение Template, которое редактируется через IDE, как и любое другое + Python скрипт, который подменяет ресурсы, файлы подписи и билдит.