Pull to refresh

Портинга и препроцессинга псто

Development for Android *
А многим ли из вас, уважаемые, не приходилось задумываться о том, как из одного набора исходников, без лишнего напряжения душевных сил собирать две независимых версии приложения — полную и не очень? Всем, поднявшим руку, скажу: ещё придётся, ой как придётся поломать голову над этим вопросом. Ну, или можно воспользоваться рецептом ниже.

Длинное, нудное и плаксивое вступление

Если вы ещё не совсем в курсе всех этих движений с баннерами и кликами в приложениях, и только собираетесь приобщиться к чистогану, кратко опишу природу проблемы. Негуманоиды из 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… Помечтать не вредно, ну а вдруг?..

Спасибо за внимание. Приветствуются предложения, дополнения и критика (даже нездоровая).
Tags:
Hubs:
Total votes 38: ↑20 and ↓18 +2
Views 1.2K
Comments Comments 32