【问题标题】:Workaround for duplicate classes in Gradle flavors and mainGradle 风格和 main 中重复类的解决方法
【发布时间】:2014-07-10 12:34:32
【问题描述】:

问题:

我想我的问题很常见。我有一个相当大的 gradle 代码库,我从中使用产品风格生成定制版本。这些产品风格通常需要来自src\main\java 的一个或多个类的定制版本。

我已阅读 Gradle 文档,并且在查看同一问题时也遇到了以下问题:
Using Build Flavors - Structuring source folders and build.gradle correctly
Build flavors for different version of same class

我理解为什么你不能在 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 块。

如何克服?

【问题讨论】:

    标签: android gradle


    【解决方案1】:

    无需硬编码 Activity 名称。

    为要加载的各个活动添加意图过滤器。

    风味 A:ActivityA.java

    风味 B:ActivityB.java

    案例: Main(Common) :带按钮的 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 打开

    【讨论】:

      【解决方案2】:

      所以这里有两个问题 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 文件处理这种事情。这样一来,Activity 和 Fragment 始终可以在不同风格之间具有相同的行为,但它们会加载不同的布局。这些布局包含自定义视图组件(它们从公共接口或抽象父类扩展,允许活动编码到该接口)。现在,不需要某些类的风味将永远不会尝试加载它们,因为它们在该特定风味加载的布局中不存在

      无论如何,我希望这在某种程度上有所帮助。如果不了解代码库的细微差别,就很难解决更广泛的问题。我的建议是从根本上重新考虑事情,并尝试让类加载器不再需要加载特定于风味的类。

      【讨论】:

      • 感谢您的回复 gmale。一个 final 变量不能被多次赋值。虽然我可以将 activityX 设为非最终类级变量(对那里的术语表示抱歉),但它仍然可以在内部类中使用,从而克服了这个问题。
      • 关于代码库,不幸的是,这是一个我“继承”来工作的大型代码库。它的结构与您所描述的方法相似,所有类最初都包含在 main 中,并且风格只包含用于“剥皮”应用程序以对其进行自定义的 XML 文件。问题是我越来越需要生产一些核心类必须以不同于最初设计的新方式或不同方式运行的风味。因此希望在风味中创建自定义版本。
      • @gmale 我正在尝试使用继承来解决这个问题(在超类中存根 fns,并在子类中实现它们)。我希望 gradle 以一种特定的产品风格将子类替换为超类,而无需使用构建配置变量进行门控。这可能吗?
      • @DanielSmith android gradle 插件并没有真正为 JAVA 文件做复杂的资源解析。意思是,“main”中的类不会被风味文件夹中的类替换。因此,选项是在每种风格中放置一个类(不包括“main”),或者将行为差异简化到可以在资源文件中表示的程度。通常,后者是更好的方法:设置它以便不同口味的主要区别是资源价值。对于“复杂”的情况,布局文件非常棒,因为您可以使用它们来加载完全不同的自定义视图对象。
      【解决方案3】:

      我相信扩展您的活动(和其他课程)是一种更清洁的解决方案。

      例如,如果您在主风格中有一个 HelpActivity,则将其设为抽象类,并在该风格中创建一个扩展 HelpActivity 的 FlavorHelpActivity。在这里,您调用 super 并添加此风味独有的所有内容。

      您需要更新每个风味中的清单以指向活动的正确风味名称 (FlavorHelpActivity),而且您的菜单项必须正确指向,因此在扩展活动中您必须覆盖 onOptionsItemSelected。

      我将尝试这个解决方案,所以也许稍后我会知道是否有缺点。

      -- 更新--

      我一直在尝试我建议的方法,但并不理想:

      • 在扩展其他活动的活动中编程有点奇怪。你有一半的逻辑不在你正在看的课程中。
      • 在使清单中的所有内容正确无误以及如何将活动与意图链接在一起方面,您需要花费相当多的开销。

      这是可行的工作方式,但我可能会回到提问者所描述的情况。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-04-18
        • 2019-09-25
        • 2010-10-18
        • 2018-09-18
        • 1970-01-01
        • 1970-01-01
        • 2018-01-05
        相关资源
        最近更新 更多