А многим ли из вас, уважаемые, не приходилось задумываться о том, как из одного набора исходников, без лишнего напряжения душевных сил собирать две независимых версии приложения — полную и не очень? Всем, поднявшим руку, скажу: ещё придётся, ой как придётся поломать голову над этим вопросом. Ну, или можно воспользоваться рецептом ниже.
Если вы ещё не совсем в курсе всех этих движений с баннерами и кликами в приложениях, и только собираетесь приобщиться к чистогану, кратко опишу природу проблемы. Негуманоиды из Google (да простят меня негуманоиды из Apple), мало того, что взяли в качестве IDE не самый прямой Eclipse, так ещё и старательно обошли стороной (и игнорируют по сей момент!) такую важную стадию сборки проекта, как препроцессинг исходника и ресурсов. А он — нужен! И пусть меня трудолюбиво проклинают любители ООП, такие же, как я, но нереально сделать dead code участок для директив import и package — здесь может спасти только препроцессор. К вящему горю, трёхглазые ещё и завязали на package всё, абсолютно всё. В результате, получается неприятная ситуация: если мы меняем в манифесте наименование пакейджа — его нужно поменять везде, во всех исходниках. А ещё необходимо перегенерировать класс R, без которого почему-то нельзя было обойтись, ну никак. А если не менять название пакейджа — приложение полностью заменяет в устройстве прежнее приложение с таким же наименованием сборки, что является весьма нешуточной потенциальной неприятностью — счастливый обладатель оплаченной версии может нечаянно сделать «апгрейд» до версии бесплатной.
Вот.
Это был крик души, а в статье мы попытаемся максимально бюджетным способом решить конкретную задачу: получить предельно простой метод переключения вариантов приложения, оставив всё «как есть» в среде разработки и не внося существенных изменений в структуру проекта. Материалы проекта при этом должны обладать минимальной избыточностью. В общем, как говорил классик, «чтобы у нас всё было, и нам ничего за это не было».
Тот, кто занимался разработкой игр для MIDP в NetBeans Mobility Pack, должен помнить прекраснейшие средства для работы с несколькими конфигурациями одного проекта. Один из компонентов MP — препроцессор Antenna, работающий в составе средства автоматизации Apache Ant. Я не буду описывать здесь Ant — я вообще плохо с ним знаком и даже не могу дать точного определения этому монструозному средству автоматизации. Я не стану исчерпывающе описывать Antenna — всё, что нужно знать для решения нашей задачи, дано ниже, а остальное можно узнать на сайтах этих приложений.
Вообще, Antenna была создана для работы с проектами мобильных приложений. Это я к тому, что там, помимо препроцессора, довольно много не нужных нам вещей. Вы не обращайте на них внимания, главное — оно выполняет то, что нам от него нужно.
Общий принцип у всего нижеописанного таков: настроенный нами препроцессор будет модифицировать исходный текст приложения, расставляя комментарии и заменяя значения там, где это будет указано соответствующими директивами. То есть, для каждой конфигурации можно писать индивидуальный код. Также модифицируются некоторые значения в AndroidManifest.xml и подразумевается возможность условной обработки любого файла xml в проекте. Поскольку все действия производятся задачами Ant, их можно инкорпорировать в Ant-скрипт build.xml, посредством которого проект может быть откомпилирован без участия Eclipse. Но это уже на ваше усмотрение.
1. Загружаем Ant отсюда, из секции «Current Release of Ant» и устанавливаем к себе на компьютер.
2. Загружаем Antenna отсюда. То, что нам нужно, выглядит примерно как «antenna-bin-1.2.1-beta.jar». Пока переименуем файл в antenna.jar, а о том, как это использовать, будет написано ниже.
3. Загружаем XmlTask (xmltask.jar) отсюда и тоже пока откладываем в сторонку.
Далее, нам необходимо подготовить проект.
4. Создаём самый обычный проект. Называем сборку, например, ru.habr.hello, проект — HelloPorting, а наша Activity пусть зовётся Main.
У нас получился проект с такой структурой:
Теперь адаптируем наш проект для препроцессинга.
5. В папке проекта создаём папку tools и помещаем туда ранее загруженные antenna.jar и xmltask.jar.
6. В папке проекта создаём файл preprocess.xml со следующим содержимым:
7. Рядом с preprocess.xml создаём файл preprocess.properties со следующим содержимым:
8. Наш класс Main должен выглядеть так:
9. Создаём новую сборку ru.habr.hello.full и в ней класс HelloPorting.java с таким содержанием:
10. Создаём новую сборку ru.habr.hello.lite и в ней класс HelloPorting.java с таким содержанием:
11. В AndroidManifest.xml, в узле manifest/application/activity изменяем аттрибут android:name на ".HelloPorting"
12. В Eclipse, в res\values открываем файл strings.xml и добавляем в него строку HelloPorting LITE с названием app_name_lite.
Подготовка проекта закончена, и мы можем проверить, как всё работает.
В папке проекта, из командной строки командуем:
или
После этого компилируем приложение и запускаем. Если всё правильно, в LogCat в секции app мы увидим строку, уведомляющую нас о том, какая конфигурация проекта построена.
В результате наших действий мы получили «двухголовый» проект, подготовка к построению любой из его конфигураций занимает считанные секунды: ровно столько, сколько нужно препроцессору, чтобы пройти по исходным текстам и немного изменить их. Нам нет нужды контролировать соответствие разных версий исходных файлов друг другу, поскольку каждый файл хранится в единственном экземпляре. Большинство различий в функционале мы можем описывать непосредственно в исходном тексте, используя директивы препроцессора.
Но необходимо учитывать некоторые новые особенности:
Теперь в каждом классе, использующем ресурсы из папки res и класс R, необходимо добавлять после всех директив импорта строку:
которая в ходе препроцессинга подставляет ниже себя актуальную для данной конфигурации строку импорта. Что так же необходимо учитывать при удалении макроса.
Классы HelloPorting.java в сборках ru.habr.hello.full и ru.habr.hello.lite, поскольку они являются «обманками» для системы и наследуют класс Main из сборки ru.habr.hello, по возможности должны оставаться пустотелыми, чтобы не было необходимости синхронизировать их при изменениях. Однако, они могут послужить своему прямому назначению, если реализовать в них код, специфичный для данной конфигурации. Например, у нас в классе Main может быть пустой или абстрактный метод createScreen, который в наследнике из сборки ru.habr.hello.lite может реализовать экран с баннером, а в ru.habr.hello.full — просто экран.
Естественно, это не идеальное решение. И даже не близкое к идеальному. Ковыряя бесчисленные мириады разной ява-тулзы, я сидел и мечтал о том, что кто-нибудь сядет и напишет плагин к Eclipse или даже к NetBeans… Помечтать не вредно, ну а вдруг?..
Спасибо за внимание. Приветствуются предложения, дополнения и критика (даже нездоровая).
Длинное, нудное и плаксивое вступление
Если вы ещё не совсем в курсе всех этих движений с баннерами и кликами в приложениях, и только собираетесь приобщиться к чистогану, кратко опишу природу проблемы. Негуманоиды из Google (да простят меня негуманоиды из Apple), мало того, что взяли в качестве IDE не самый прямой Eclipse, так ещё и старательно обошли стороной (и игнорируют по сей момент!) такую важную стадию сборки проекта, как препроцессинг исходника и ресурсов. А он — нужен! И пусть меня трудолюбиво проклинают любители ООП, такие же, как я, но нереально сделать dead code участок для директив import и package — здесь может спасти только препроцессор. К вящему горю, трёхглазые ещё и завязали на package всё, абсолютно всё. В результате, получается неприятная ситуация: если мы меняем в манифесте наименование пакейджа — его нужно поменять везде, во всех исходниках. А ещё необходимо перегенерировать класс R, без которого почему-то нельзя было обойтись, ну никак. А если не менять название пакейджа — приложение полностью заменяет в устройстве прежнее приложение с таким же наименованием сборки, что является весьма нешуточной потенциальной неприятностью — счастливый обладатель оплаченной версии может нечаянно сделать «апгрейд» до версии бесплатной.
Вот.
Это был крик души, а в статье мы попытаемся максимально бюджетным способом решить конкретную задачу: получить предельно простой метод переключения вариантов приложения, оставив всё «как есть» в среде разработки и не внося существенных изменений в структуру проекта. Материалы проекта при этом должны обладать минимальной избыточностью. В общем, как говорил классик, «чтобы у нас всё было, и нам ничего за это не было».
Тот, кто занимался разработкой игр для MIDP в NetBeans Mobility Pack, должен помнить прекраснейшие средства для работы с несколькими конфигурациями одного проекта. Один из компонентов MP — препроцессор Antenna, работающий в составе средства автоматизации Apache Ant. Я не буду описывать здесь Ant — я вообще плохо с ним знаком и даже не могу дать точного определения этому монструозному средству автоматизации. Я не стану исчерпывающе описывать Antenna — всё, что нужно знать для решения нашей задачи, дано ниже, а остальное можно узнать на сайтах этих приложений.
Вообще, Antenna была создана для работы с проектами мобильных приложений. Это я к тому, что там, помимо препроцессора, довольно много не нужных нам вещей. Вы не обращайте на них внимания, главное — оно выполняет то, что нам от него нужно.
Начнём.
Общий принцип у всего нижеописанного таков: настроенный нами препроцессор будет модифицировать исходный текст приложения, расставляя комментарии и заменяя значения там, где это будет указано соответствующими директивами. То есть, для каждой конфигурации можно писать индивидуальный код. Также модифицируются некоторые значения в AndroidManifest.xml и подразумевается возможность условной обработки любого файла xml в проекте. Поскольку все действия производятся задачами Ant, их можно инкорпорировать в Ant-скрипт build.xml, посредством которого проект может быть откомпилирован без участия Eclipse. Но это уже на ваше усмотрение.
1. Загружаем Ant отсюда, из секции «Current Release of Ant» и устанавливаем к себе на компьютер.
2. Загружаем Antenna отсюда. То, что нам нужно, выглядит примерно как «antenna-bin-1.2.1-beta.jar». Пока переименуем файл в antenna.jar, а о том, как это использовать, будет написано ниже.
3. Загружаем XmlTask (xmltask.jar) отсюда и тоже пока откладываем в сторонку.
Далее, нам необходимо подготовить проект.
4. Создаём самый обычный проект. Называем сборку, например, ru.habr.hello, проект — HelloPorting, а наша Activity пусть зовётся Main.
У нас получился проект с такой структурой:
Теперь адаптируем наш проект для препроцессинга.
5. В папке проекта создаём папку tools и помещаем туда ранее загруженные antenna.jar и xmltask.jar.
6. В папке проекта создаём файл preprocess.xml со следующим содержимым:
<?xml version="1.0" encoding="Utf-8"?>
<project name="Preprocess source" default="lite" basedir=".">
<property file="preprocess.properties" />
<property name="wtk.home" value="." />
<property name="source.path" value="src" />
<taskdef resource="antenna.properties" classpath="tools\antenna.jar" />
<taskdef name="xmltask" classname="com.oopsconsultancy.xmltask.ant.XmlTask" classpath="tools\xmltask.jar" />
<target name="full">
<echo>Preprocessing FULL VERSION</echo>
<wtkpreprocess
verbose="false"
version="2"
srcdir="${source.path}"
destdir="${source.path}"
symbols="distr=full" />
<xmltask source="AndroidManifest.xml" dest="AndroidManifest.xml">
<attr path="manifest" attr="package" value="${androidmanifest.package.full}" />
<attr path="manifest/application" attr="android:label" value="${androidmanifest.application.label.full}" />
<attr path="manifest/application/activity" attr="android:label" value="${androidmanifest.application.label.full}" />
</xmltask>
</target>
<target name="lite">
<echo>Preprocessing LITE VERSION</echo>
<wtkpreprocess
verbose="false"
version="2"
srcdir="${source.path}"
destdir="${source.path}"
symbols="distr=lite" />
<xmltask source="AndroidManifest.xml" dest="AndroidManifest.xml">
<attr path="manifest" attr="package" value="${androidmanifest.package.lite}" />
<attr path="manifest/application" attr="android:label" value="${androidmanifest.application.label.lite}" />
<attr path="manifest/application/activity" attr="android:label" value="${androidmanifest.application.label.lite}" />
</xmltask>
</target>
</project>
7. Рядом с preprocess.xml создаём файл preprocess.properties со следующим содержимым:
# Full version properties
androidmanifest.package.full=ru.habr.hello.full
androidmanifest.application.label.full=@string/app_name
# Lite version properties
androidmanifest.package.lite=ru.habr.hello.lite
androidmanifest.application.label.lite=@string/app_name_lite
8. Наш класс Main должен выглядеть так:
package ru.habr.hello;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
//#expand import ru.habr.hello.%distr%.*;
public class Main extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//#if distr == "lite"
Log.d("app", "Lite version");
//#elif distr == "full"
Log.d("app", "Full version");
//#else
Log.d("app", "Whatelse version");
//#endif
}
}
9. Создаём новую сборку ru.habr.hello.full и в ней класс HelloPorting.java с таким содержанием:
package ru.habr.hello.full;
import ru.habr.hello.*;
public class HelloPorting extends Main {
}
10. Создаём новую сборку ru.habr.hello.lite и в ней класс HelloPorting.java с таким содержанием:
package ru.habr.hello.lite;
import ru.habr.hello.*;
public class HelloPorting extends Main {
}
11. В AndroidManifest.xml, в узле manifest/application/activity изменяем аттрибут android:name на ".HelloPorting"
12. В Eclipse, в res\values открываем файл strings.xml и добавляем в него строку HelloPorting LITE с названием app_name_lite.
Подготовка проекта закончена, и мы можем проверить, как всё работает.
В папке проекта, из командной строки командуем:
ant -f preprocess.xml lite
или
ant -f preprocess.xml full
После этого компилируем приложение и запускаем. Если всё правильно, в LogCat в секции app мы увидим строку, уведомляющую нас о том, какая конфигурация проекта построена.
Что дальше.
В результате наших действий мы получили «двухголовый» проект, подготовка к построению любой из его конфигураций занимает считанные секунды: ровно столько, сколько нужно препроцессору, чтобы пройти по исходным текстам и немного изменить их. Нам нет нужды контролировать соответствие разных версий исходных файлов друг другу, поскольку каждый файл хранится в единственном экземпляре. Большинство различий в функционале мы можем описывать непосредственно в исходном тексте, используя директивы препроцессора.
Но необходимо учитывать некоторые новые особенности:
Теперь в каждом классе, использующем ресурсы из папки res и класс R, необходимо добавлять после всех директив импорта строку:
//#expand import ru.habr.hello.%distr%.*;
которая в ходе препроцессинга подставляет ниже себя актуальную для данной конфигурации строку импорта. Что так же необходимо учитывать при удалении макроса.
Классы HelloPorting.java в сборках ru.habr.hello.full и ru.habr.hello.lite, поскольку они являются «обманками» для системы и наследуют класс Main из сборки ru.habr.hello, по возможности должны оставаться пустотелыми, чтобы не было необходимости синхронизировать их при изменениях. Однако, они могут послужить своему прямому назначению, если реализовать в них код, специфичный для данной конфигурации. Например, у нас в классе Main может быть пустой или абстрактный метод createScreen, который в наследнике из сборки ru.habr.hello.lite может реализовать экран с баннером, а в ru.habr.hello.full — просто экран.
Заключение.
Естественно, это не идеальное решение. И даже не близкое к идеальному. Ковыряя бесчисленные мириады разной ява-тулзы, я сидел и мечтал о том, что кто-нибудь сядет и напишет плагин к Eclipse или даже к NetBeans… Помечтать не вредно, ну а вдруг?..
Спасибо за внимание. Приветствуются предложения, дополнения и критика (даже нездоровая).