Конвейерное производство Android приложений

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

66a7a47255d24b7c85a02c885309f40f.PNG

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


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


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 (кинобар, реклама, аренда залов, акции).
76acf9bc897442a2a953a8808217dc5e.png

Дизайн


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


Аналогично хранится информация о видимости элементов, дизайне и прочем.

Пример изменения приложения только одними ресурсами
5c73c475b940476d9cca9c8858b53861.png

Мастер создания приложений.


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

Спасибо за внимание!

© Habrahabr.ru