Обходной путь для повторяющихся классов в Gradle ароматах и ​​основных

Проблема:

Я предполагаю, что моя проблема довольно распространена. У меня довольно большая база кода gradle, из которой я создаю настраиваемые версии с использованием вкусов продукта. Этим товарам часто требуется индивидуальная версия одного или нескольких классов из src\main\java.

Я прочитал документацию gradle и также столкнулся со следующими вопросами, рассматривая ту же проблему:
Использование Build Flavors - структурирование исходных папок и правильное создание .gradle
Создавайте ароматы для разных версий того же класса

Я понимаю, почему вы не можете определить тот же класс в src\main\java, а также в ваших вкусах, однако решение о перемещении класса из src\main\java в ваш вкус продукта имеет довольно серьезный недостаток. Когда вы перемещаете класс из src\main\java в свой последний вкус, чтобы настроить его, вам также нужно переместить копию оригинальной нестандартной версии этого класса в любой другой предыдущий продукт, или они больше не будут построены.

Вам может потребоваться только один раз выбрать один или два разных класса из оригиналов (а затем перефразировать эти классы в каталогах вкусов), но со временем количество перемещенных классов будет построено, а число, оставшееся в src\main\java будет уменьшаться каждый раз, когда вы это сделаете. В конце концов большинство классов будут в ароматах (хотя большинство будет копиями оригиналов), а src\main\java будет почти пустым, что приведет к победе цели всей структуры сборки gradle.
Кроме того, вам нужно будет сохранить "стандартный" вкус, который вы можете клонировать каждый раз, когда вы начинаете новый вкус, поэтому вы знаете, что начинаете со всех классов в соответствии с исходной базой кода.

Мое начальное решение:

Использовать поля в BuildConfig для определения использования пользовательского класса или нет:

buildConfigField 'boolean', 'CUSTOM_ACTIVITY_X', 'true'

Затем вы можете использовать код, например:

final Intent intent = new Intent();
...
if (BuildConfig.CUSTOM_ACTIVITY_X) {
   intent.setClass(ThisActivity.this, CustomActivityX.class); 
} else {
   intent.setClass(ThisActivity.this, DefaultActivityX.class);  
}
startActivity(intent);

Каждому аромату все равно понадобится копия CustomActivityX, но она может быть просто пустым пустым классом в ароматах, где вы знаете, что он не будет использоваться. Это означает, что ваши версии по умолчанию для классов всегда сохраняются в src\main\java.

Улучшенный обходной путь:

При попытке избавиться от необходимости создания фиктивного CustomActivityX в каждом другом вкусе, я посмотрел на использование Class.forName().
Например:

final Class activityX;
if (BuildConfig.CUSTOM_ACTIVITY_X) {
    try {
        activityX = Class.forName("CustomActivityX");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
} else {
    activityX = DefaultActivityX.class;
}

final Intent intent = new Intent();
...
intent.setClass(ThisActivity.this, activityX); 
startActivity(intent);

Однако это, очевидно, приводит к тому, что "activityX может не быть инициализировано" при попытке использовать его из-за блока try/catch.

Как это можно преодолеть?

Ответ 1

Итак, здесь есть две проблемы: 1) ошибка кодирования в вашем основном обходном пути; 2) более широкая проблема, которую вы пытаетесь решить.

Я могу помочь больше с первой проблемой, чем второй. Все, что вам нужно сделать, это инициализировать вашу переменную. Вы спросили: "Как это можно преодолеть?" Я считаю, что это сделает трюк:

Class activityX = null;  //remove 'final' and initialize this to null, then null-check it later
if (BuildConfig.CUSTOM_ACTIVITY_X) {
    try {
        activityX = Class.forName("CustomActivityX");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
} else {
    activityX = DefaultActivityX.class;
}

final Intent intent = new Intent();
...
if(activityX != null) {
    intent.setClass(ThisActivity.this, activityX); 
    startActivity(intent);
}

Теперь, в отношении более широкой проблемы, которую вы решаете, довольно ясно, что в приведенном выше коде есть некоторые запахи кода, которые сигнализируют, что может быть лучший способ. Я использовал классы, специфичные для вкуса, без необходимости копировать их во все другие вкусы. В этих случаях другие вкусы не выполняли код, основанный на этих классах. Например, представьте себе "платную" версию, где "свободная" версия просто никогда не загружает некоторые из классов, доступных для платных пользователей.

Поэтому я думаю, что проблема возникает только в том случае, если все вкусы пытаются загрузить класс, о котором идет речь. Трудно предложить альтернативу, не понимая общую кодовую базу. Однако я бы посоветовал вам попробовать использовать наследование, абстрактные классы или интерфейсы для решения вашей проблемы.

Первое, что я хотел бы исследовать: действительно ли Activity действительно самая маленькая единица кода/поведения, которую вам нужно переопределить? Я подозреваю, что это не так. Возможно, вы можете создать BaseActivity, который имеет весь шаблонный код, а затем изолировать код, специфичный для вкуса, к точным компонентам, которые его требуют.

Например, частый шаблон, который я использовал, - обрабатывать подобные вещи через XML файлы. Таким образом, действия и фрагменты всегда могут иметь одинаковое поведение по вкусам, но они загружают разные макеты. Эти макеты содержат настраиваемые компоненты представления (которые простираются от общего интерфейса или абстрактного родительского класса, что позволяет операции кодировать этот интерфейс). Теперь вкусы, которым не нужны определенные классы, никогда не будут пытаться загрузить их, потому что они не существуют в макете, загруженной этим особым вкусом.

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

Ответ 2

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

Например, если у вас есть HelpActivity в главном вкусе, вы делаете это абстрактным классом, и в аромате вы создаете FlavorHelpActivity, который расширяет HelpActivity. Здесь вы называете супер и добавляете все, что уникально для этого вкуса.

Вам нужно обновить манифест в каждом аромате, чтобы указать на правильное название аромата (FlavorHelpActivity), а также ваши пункты меню должны указывать правильно, поэтому в расширенных действиях вы должны переопределить onOptionsItemSelected.

Я собираюсь попробовать это решение, поэтому, возможно, позже я смогу сказать, есть ли недостатки.

- Обновление -

Я пробовал свой предложенный подход, и он не идеален:

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

Он умеет работать, но я, вероятно, вернусь к ситуации, описанной человеком, который задал вопрос.

Ответ 3

Нет необходимости в жестком коде Названия действий.

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

Аромат A: ActivityA.java

Вкус B: ActivityB.java

Случай: Основной (общий): BaseActivity с кнопкой. При нажатии кнопки для аромата A следует перейти к ActivityA, а для аромата B следует перейти к ActivityB.

Проявлять аромат A:

<activity
            android:name="com.abc.ActivityA"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="com.abc.openDetailsActivity" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

Манифест для вкуса B:

<activity
            android:name="com.abc.ActivityB"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="com.abc.openDetailsActivity" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

Оба должны иметь одно и то же действие в манифесте.

Теперь из вызова BaseActivity выполните следующие действия:

Intent i = new Intent();
            i.setAction("com.abc.openDetailsActivity");
            startActivity(i);

Если Build Variant - A, ActivityA откроется из Flavor A и если Build Variant - B, ActivityB откроется из Flavor B