воскресенье, 20 февраля 2011 г.

Как разделить Android приложение на Pro и Free версии.

Если вы разрабатываете бесплатное приложение под Android, то это совсем не значит, что оно не принесет вам доход. Путей монетаризации достаточно: показ рекламы, пожертвования пользователей, продажа специальной "Pro" версии приложения, в которой нет рекламы и/или есть дополнительный функционал.

Типичный сценарий таков. Вы разрабатываете Free версию приложения, в которой реклама есть. И предлагаете приобрести Pro версию, в которой рекламы нет. Бесплатность приложения привлекает большое количество пользователей. Больше пользователей - больше кликов по рекламе, больше покупок Pro версии.

Возникает чисто технический вопрос: как сделать две версии приложения - Pro и Free, - на базе одних и тех же исходных кодов? Вот об этом и поговорим.

Традиционные способы разделения Pro и Free версий

Вопрос разделения Pro и Free версий Android-приложений обсуждается на многих форумах (вот, например, ветка на stackoverflow). Как правило, предлагаются следующие варианты:
  • Вводить runtime или compile time переключатели Pro/Free функциональности.
  • Использовать git. Вести в основной ветке разработку Pro-версии, и в отдельной ветке держать измененную Free-версию. При сборке очередного билда делать для Free-версии команду rebase, накатывая Free-изменения на основную Pro-версию.
  • Создавать два отдельных проекта, использующих общую Android library.
  • Вместо Pro версии разрабатывать и продавать "unlocker" приложение. Если "unlocker" приложение на устройстве установлено, то ваше приложение работает как "Pro". Если не установлено - то как "Free". (Правда, такой подход подразумевает скорее разработку двух разных приложений, чем две версии одного и того же приложения, т.е. это несколько другое.)
Ни один из перечисленных подходов мне не подошел: все либо не удобны, либо не надежны, либо возможностей не хватает. Пришлось искать другой вариант.

Основные отличия Pro и Free версий

В чем отличия Pro и Free версий на уровне исходных кодов?
  • Приложения называются по разному. Например, MyApplication и MyApplication Pro
  • У приложений разные иконки: icon.png, icon_pro.png
  • Во Free приложении может присутствовать кнопка "Donate" ("Bue Pro"). Два основных места, где она присутствует: главный экран приложения и option menu. На уровне исходников это означает, что в основной Activity приложения должен присутствовать код для отображения и обработки нажатия такой кнопки, а меню должны задаваться в ресурсах разными xml-файлами.
  • Во Free приложении может присутстовать код для отображения рекламного баннера.
  • Pro версия может содержать функции, которых во Free версии нет. Т.е. в Pro-проект должны входить дополнительные классы и файлы ресурсов.
  • Pro и Free приложения должны иметь разные названия пакетов, например: com.dummy.myapp и com.dummy.myapp.pro
В целом, отличия сводятся к следующим:
  • Отличная структура директорий (из-за отличий в названии пакета).
  • Отличия в значениях некоторых атрибутов в манифесте и других xml файлах (названия иконок, название программы и т.д.)
  • Разный набор файлов (некоторые файлы должны быть только во Free версии, другие только в Pro).
  • Небольшие отличия в коде (например, в Pro версии должен использоваться один идентификатор меню, во Free - другой; в Pro версии надо вызвать одну функцию, во Free - другую).

Утилита apfsplitter

Подход, к которому я в итоге пришел, достаточно прост: разрабатывать единую базовую версию, и генерировать на ее основе Pro и Free версии с помощью специальной (самописной) утилиты AndroidProFreeSplitter (сокращенно, apfsplitter).

Имеется единственный базовый проект. На нем можно разрабатывать и отлаживать как Pro, так и Free версию, используя булевую переменную IS_PRO как переключатель. Когда же нужно собрать релиз, AndroidProFreeSplitter генерирует на основе базового два новых проекта: Pro и Free. Генерация сводится к копированию дерева проекта, исключению ненужных файлов и изменению некоторых файлов проекта. Порядок копирования, исключения и изменения файлов определяется набором правил. Правила задаются в конфигурационном файле и представляют, по большому счету, список регулярных выражений + файловых масок.

Этот подход напоминает git-вариант. Только изменения "накатываются" на базовую версию не с помощью git, а с помощью специальной утилиты.

Тестовый проект DemoProFreeSplitter

Утилита apfsplitter является достаточно общей и не накладывает никаких ограничений на структуру проекта. Но на практике такие ограничения накладывать приходится - иначе как описать набор правил для преобразования файлов?

В реальном проекте, я ввел ряд соглашений об именовании Pro-Free зависимых файлов и функций и прописал соответствующий набор правил в конфигурационном файле. Вот эти соглашения.

Версии

У нас три версии - базовая, pro и free. Базовая версия может работать как pro и как free в зависимости от значения переменной Version.IS_PRO. При генерации Pro/Free проектов apfsplitter автоматические выставляет корректное значение IS_PRO.

Именование файлов

Файлы, которые должны быть включены только во free-версию, заканчиваются на "_free". Файлы, которые должны быть включены только в pro-версию, заканчиваются на "_pro". Пример: файл "icon_pro.png" будет скопирован только в pro-версию.

Макросы, аналоги #ifdef/#endif

Да, макросы это плохо. И использовать их нужно крайне осторожно. Тем не менее, макросы могут пригодиться. Например, чтобы удалять Pro-код из Free-версии и Free-код из Pro-версии.

Макросы введены в следующем виде:
//#startBASE 
  //здесь код который будет выполняться только в базовой версии
  //в Pro и Free он будет закомментирован
//#endBASE

//#startPRO
  //здесь код который будет выполняться только в pro-версии
  //в Free он будет закомментирован
//#endPRO

//#startPRO
  //здесь код который будет выполняться только во free-версии
  //в Pro он будет закомментирован
//#endPRO
Обращаю внимание, что ненужный код не удаляется, а комментируется. Хотя в принципе, в конфигурационном файле apfsplitter можно изменить соответствующие регулярные выражения и код будет удаляться.

Именование функций

Если у нас есть Pro/Free-зависимый код, то он оформляется в виде трех функций:
//#startBASE 
public void prepareDonateButton_ProFree() {
//эта функция работает только в базовой версии
//в Free/Pro проектах она закомментирована.
 if (Version.IS_PRO) {
  prepareDonateButton_Pro();
 } else {
  prepareDonateButton_Free();
 }
}
//#endBASE

//#startFREE 
public void prepareDonateButton_Free() {
   //показываем кнопку Donate во Free версии
}
//#endFREE

//#startPRO 
public void prepareDonateButton_Pro() {
  //не показываем кнопку Donate в Pro версии
}
//#endPRO 
В базовой версии будут присутствовать все три функции. В Pro версии все вызовы prepareDonateButton_ProFree будут заменены на вызовы prepareDonateButton_Pro, а функции prepareDonateButton_ProFree и prepareDonateButton_Free закомментированы (с помощью макросов). Аналогично, во Free версии все вызовы prepareDonateButton_ProFree будут заменены на вызовы prepareDonateButton_Free, а функции prepareDonateButton_ProFree и prepareDonateButton_Pro закомментированы.

Класс Version

В проекте имеется вот такой класс Version:
public class Version {
 public static final boolean IS_PRO = false;
 public static int GetFreeProId(int idFree, int idPro) {
   return IS_PRO ? idPro : idFree; 
 }
 public static int GetFreeId(int idFree) {
   return IS_PRO ? 0 : idFree; 
 }
 public static int GetProId(int idPro) {
   return IS_PRO ? idPro : 0; 
 }
}
Когда мы работаем в базовой версии, то переменная IS_PRO определяет, какая из версий Pro/Free компилируется. При создании Pro/Free версий apfsplitter принудительно устанавливает значение константы IS_PRO в true или false.

Функции GetFreeProId, GetFreeId и GetProId нужны, чтобы корректно обращаться к ресурсам. Предположим, у нас есть два варианта menu: R.menu.menu_free и R.menu.menu_pro. Чтобы вызвать меню мы пишем в базовой версии такой код:
inflater.inflate(       
    Version.GetProFreeId(R.menu.menu_free, R.menu.menu_pro) 
  , menu);
В Pro версии вызов GetProFreeId будет заменен на
inflater.inflate(R.menu.menu_pro, menu). В Free версии на inflater.inflate(R.menu.menu_free, menu). Такой финт нужен потому, что в Pro версии идентификатора R.menu.menu_free может не существовать, а во Free версии может не существовать R.menu.menu_pro (напомню, что файлы оканчивающиеся на _free не попадают в Pro версию, так что menu_free.xml не попадет в Pro версию и идентификатора R.menu.menu_free не будет; аналогичная ситуация с menu_pro).

Функции GetFreeId и GetProId работают аналогично. В той версии, где нужного идентификатора нет, вместо него будет стоять ноль.

Тестовый проект

Тестовый проект DemoProFreeSplitter демонстрирует использование указанных соглашений именования и утилиты apfsplitter.

Командный файл для генерации Pro и Free версий тестового проекта выглядит так:
apfsplitter.exe "sources" "versions\pro" "apfs_config.xml" pro
apfsplitter.exe "sources" "versions\free" "apfs_config.xml " !pro

Директория "sources" содержит базовую версию проекта. Результаты записываются в "versions\pro" и "versions\free". Конфигурационный файл с правилами разбиения называется apfs_config.xml. Последним параметром передается тег версии - "pro" или, для free версии,"!pro".

Выводы

Итак, с помощью утилиты apfsplitter можно без особых хлопот генерировать Pro и Free версии Android приложения на основе единого набора исходных кодов - "базовой" версии. Сама базовая версия может работать как Pro и Free, так что ее можно использовать при разработке, отладке и тестировании как Pro так и Free версии.

Тестовый проект DemoProFreeSplitter демонстрирует возможности apfsplitter. В проект входит конфигурационный файл apfsplitter. Чтобы можно было использовать этот файл в реальном проекте, необходимо придерживаться в этом проекте ряда простых соглашений в именовании Pro/Free зависимых файлов и функций. Кроме того, необходимо изменить последнее правило:
<replace if="target:pro">   
 <files>.project</files>
 <search>projectDescription/name</search>
 <replace>My Application Pro Title</replace>
<kind>xml</kind>
</replace>
Данное правило заменяет в Pro-версии название проекта. Вам необходимо либо заменить "My Application Pro Title" на что-нибудь другое, либо вообще удалить это правило из конфигурационного файла.

Утилита apfsplitter достаточно универсальна и не привязана к каким-либо соглашениям. Соглашения, описанные выше, оказались удобными для меня, но это лишь один из возможных вариантов. Естественно, их можно доработать, изменить или полностью заменить на другие. Для этого потребуется отредактировать конфигурационный файл apfsplitter. Описание его структуры - тема отдельного разговора, к которому я вернусь в дальнейшем.

Ссылки и исходники

Скачать утилиту apfsplitter и тестовый проект DemoProFreeSplitter в виде 7z-архива.

Просмотреть исходные коды тестового проекта DemoProFreeSplitter.

Просмотреть исходные коды apfsplitter.

4 комментария:

  1. Зачем макросы?! Введите public static final boolean и всё! Proguard сам вырежет неиспользуемые куски кода, саму константу и класс, её содержащий (если он останется пустой)

    ОтветитьУдалить
  2. Конечно, макросы - далеко не лучшая идея. Однако, если я не ошибаюсь, Proguard вырезает неиспользуемый код только из скомпилированного приложения. Здесь же, при генерации Free/Pro-версий выбрасывается часть файлов. В результате некоторые функции могут перестать компилироваться и до Proguard дело не дойдет. Макросы нужны исключительно для того, чтобы выбросить такой некомпилируемый код.

    Ну а если выбрасывание файлов не нарушает компиляцию - макросы не нужны.

    ОтветитьУдалить
  3. Неплохо бы сделать отметку, что нельзя делать "про" папку вложенную во "фри". Прошлый раз попался на это.

    ОтветитьУдалить
    Ответы
    1. Я надеюсь, сейчас Google допилит Android Studio и необходимость в подобных утилитах вообще отпадет, т.к. в Gradle возможность сборки нескольких разных версий приложения заложена изначально.

      Удалить