从零开始的Android新项目之组件化实践

组件化是什么

组件化,相对于容器化(插件),是一种没有黑科技的相互隔离的并行开发方式。为了了解组件化,不得不先说一下插件化。

为什么我们需要插件化

现代 Android 开发中,往往会堆积很多的需求进项目,超过 65535 后,MultiDex、插件化都是解决方案。但方法数不是引入插件化的唯一原因,更多的时候,引入插件化有另外几个理由:

  • 满足产品经理随时上线的需求(注意,这在国外是命令禁止的,App store 和 Google Play 都不允许这种行为,支付宝因此被 Google Play 下架过,仔细想想,如果任何应用都能在线上替换原来的行为,审查还有什么用?)。
  • 团队比较有钱,愿意养人做这个。技术人员觉得不做业务简直太棒了,可以安心研究技术。
  • 并行开发,常见于复杂的各种东西往里塞的大型应用,比如 —— 手Q、手空、手淘、支付宝、大众点评、携程等等。这些团队的 Android 开发动辄是数百人,并分成好几个业务组,如此要并行开发便需要解耦各个模块,避免互相依赖。而且代码一多吧,编译也会很慢(我们公司现在的工程已经需要 5 - 6 分钟了,手空使用 ant 都需要 5 分钟,而 手Q 使用 ant 则需要 10 分钟,改成 gradle 的话姑且乘个2,都是几十分钟的级别)。插件化可以加快编译速度,从而提高开发效率。

其实真正的理由就只有第三个(我相信业务技术人员也不会真的想无休止地发版本,除了一些分 架构组/业务组 的地方,架构组会不考虑业务组的感受)。在知乎上,小梁也有对此作出回答:怎么将 Android 程序做成插件化的形式?,建议去读一下。

本篇里不多说插件化的工作原理,建议移步去别处学习,直接看源码也可以,像 atlas 这样 Hook 构成的插件框架可能阅读起来会有些困难,其他还好。

插件化的恶

躺不完的坑。
—— 即便是一些做了很多年的插件化框架,依然在不断躺坑,更何况是使用他们的开发者,简直是花式中枪。

发不完的版本。
—— 什么?赶不上?没事,迟些可以单独发版本。这回你可真是搬砖的码农了。

这个在我的插件里是好的呀。
—— 在各自的壳里运行很完美,然而集成后各种问题不断,甚至一启动就 ANR。

版本带来的问题。
—— 因为要动态发版本,所以每个插件自然需要有各种版本。什么?那个不对?肯定是你引用的版本错啦。更何况发版本本身就是个让人很心累的事情。

等等等等,不赘述。垃圾插件,还我青春。

组件化 VS 插件化

组件化带来的,是一个没有黑科技的插件化。应用了 Android 原有的技术栈以及 Gradle 的灵活性,失去的是动态发版本的能力,其他则做得比插件化更好。因为没有黑科技,所以不会有那么多黑科技和各种 hook 导致的坑,以及为了规避它们必须小心翼翼遵守的开发规范,几乎和没有使用插件化的 Android 开发一模一样。

而我们需要关心的,只是如何做好隔离,如何更好地设计,以及提高开发效率与产品体验。

Take Action

Gradle

组件化的基本就是通过 gradle 脚本来做的。

通过在需要组件化的业务 module 中:

 

1

2

3

4

5

 

if (isDebug.toBoolean()) {

apply plugin: 'com.android.application'

} else {

apply plugin: 'com.android.library'

}

 

并在业务 module 中放一个 gradle.properties:

 

1

 

isDebug=false

 

如此,当我们设置 isDebug 为 true 时,则这个 module 将会作为 application module 编译为 apk,否则 为 library module 编译为 aar。

下面的 gradle 是我们的一个组件化业务 module 的完整 build.gralde:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

 

println isDebug.toBoolean()

if (isDebug.toBoolean()) {

apply plugin: 'com.android.application'

} else {

apply plugin: 'com.android.library'

}

apply plugin: 'me.tatarka.retrolambda'

apply plugin: 'com.neenbedankt.android-apt'

android {

compileSdkVersion rootProject.ext.compileSdkVersion

buildToolsVersion rootProject.ext.buildToolsVersion

defaultConfig {

minSdkVersion rootProject.ext.minSdkVersion

targetSdkVersion rootProject.ext.targetSdkVersion

versionCode rootProject.ext.versionCode

versionName rootProject.ext.versionName

multiDexEnabled true

if (isDebug.toBoolean()) {

ndk {

abiFilters "armeabi-v7a", "x86"

}

}

}

compileOptions {

sourceCompatibility rootProject.ext.javaVersion

targetCompatibility rootProject.ext.javaVersion

}

lintOptions {

abortOnError rootProject.ext.abortOnLintError

checkReleaseBuilds rootProject.ext.checkLintRelease

}

buildTypes {

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

}

}

dataBinding {

enabled = true

}

if (isDebug.toBoolean()) {

splits {

abi {

enable true

reset()

include 'armeabi-v7a', 'x86'

universalApk false

}

}

}

}

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile project(':lib_stay_base')

apt rootProject.ext.libGuava

apt rootProject.ext.libDaggerCompiler

}

 

各位根据实际需要参考修改即可。

这里另外提供一个小诀窍,为了对抗 Android Studio 的坑爹,比如有时候改了 gradle,sync 后仍然没法直接通过 IDE 启动 module app,可以修改 settings.gradle,比如:

 

1

2

3

4

5

6

7

8

9

 

include ':app'

include ':data'

include ':domain'

include ':module_setting'

include ':module_card'

include ':module_discovery'

include ':module_feed'

include ':lib_stay_base'

// 省略一堆 sdk 库

 

可以把不需要的 module 都给先注释了(只留下需要的 module,lib_base,以及 sdk),尤其是 app module。然后基本上就没问题。

Manifest

一个很常见的需求就是,当我作为独立业务运行的时候,manifest 会不同,比如会多些 activity(用来套的,或者测试调试用的),或者 application 不同,总之会有些细微的差别。

一个简单的做法是:

 

1

2

3

4

5

6

7

8

9

 

sourceSets {

main {

if (isDebug.toBoolean()) {

manifest.srcFile 'src/debug/AndroidManifest.xml'

} else {

manifest.srcFile 'src/release/AndroidManifest.xml'

}

}

}

 

这样在编译时使用两个 manifest,但是这样一来,两者就有很多重复的内容,会有维护、比较的成本。

我们可以利用自带 flavor manifest merge,分别对应 debug/AndroidManifest.xml, main/AndroidManifest.xml, 以及 release/AndroidManifest.xml。

main 下的 manifest 写通用的东西,另外 2 个分别写各自独立的,通常 release 的 manifest 只是一个空的 application 标签,而 debug 的会有 application 和调试用的 activity(你总得要有个启动 activity 吧)及权限。

这里有一个小 tip,就是在 release 的 manifest 中,application 标签下尽量不要放任何东西,只是占个位,让上面去 merge,否则比如一个 module supportsRtl 设置为了 true,另一个 module 设置为了 false,就不得不去做 override 了。

Wrapper

看一个 debug manifest 的例子:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

 

<manifest package="com.amokie.stay.module.card"

xmlns:android="http://schemas.android.com/apk/res/android">

<application

android:name="com.amokie.stay.base.BaseApplication"

android:allowBackup="true"

android:alwaysRetainTaskState="true"

android:hardwareAccelerated="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:largeHeap="true"

android:sharedUserId="com.amokie.stay"

android:supportsRtl="true"

android:theme="@style/AppTheme">

<activity android:name=".WrapActivity">

<intent-filter>

<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>

</intent-filter>

</activity>

</application>

</manifest>

 

这里的 WrapActivity 就是我们所谓的 wrapper 了。

因为入口页可能是一个 fragment,所以就需要一个 activity 来包一下它,并作为启动类。

Application

BaseApplication 继承了 MultiDexApplication,而真正最后集成的 Application 则继承自
BaseApplication,并添加了一些集成时需要做的事情(比如监控、埋点、Crash上报的初始化)。

但大部分的仍会放在 BaseApplication,比如图片库、React Native、Log 等。然后各个 Module 则直接使用 BaseApplication,免去各自去写初始化的代码。

当然,如果一定想复杂化,也可以专门搞个 library module 做初始化,但我个人不建议过度复杂的设计。

可以先阅读阿布的总结文章:项目组件化之遇到的坑,也感谢小梁抛砖引玉的 Demo

我这边简单也讲一讲。

Data Binding

见我上一篇写到的记一次 Data Binding 在 library module 中遇到的大坑,简单说起来就是 data binding 在 library module 的支持有一个 bug,就是不支持 get ViewModel 的方法,只能 set 进去,从而导致做好模块化的 module 在作为 application 可以独立运行后,作为 library module 无法通过编译。

另外碰到一个问题,就是时不时会有如下的报错(出现在集成 application 的时候,且并不是必现):

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

 

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter]

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] FAILURE: Build completed with 3 failures.

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter]

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] 1: Task failed with an exception.

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] -----------

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] * What went wrong:

10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter] Execution failed for task ':module_user:dataBindingProcessLayoutsRelease'.

10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter] > -1

10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter]

10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter] * Exception is:

10:26:29.624 [ERROR] [org.gradle.BuildExceptionReporter] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':module_user:dataBindingProcessLayoutsRelease'.

10:26:29.624 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)

10:26:29.625 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)

10:26:29.625 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)

10:26:29.626 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:66)

10:26:29.626 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)

10:26:29.627 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:52)

10:26:29.627 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)

10:26:29.627 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:203)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:185)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:66)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:50)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.taskgraph.ParallelTaskPlanExecutor.process(ParallelTaskPlanExecutor.java:47)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:110)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:37)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.DefaultBuildExecuter.access$000(DefaultBuildExecuter.java:23)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.DefaultBuildExecuter$1.proceed(DefaultBuildExecuter.java:43)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)

10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:30)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher$4.run(DefaultGradleLauncher.java:153)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.Factories$1.create(Factories.java:22)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:53)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:150)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher.access$200(DefaultGradleLauncher.java:32)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:98)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:92)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:63)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:92)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:83)

10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:99)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:28)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:48)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:30)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:81)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:46)

10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:52)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:37)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:26)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:34)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:74)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:72)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.util.Swapper.swap(Swapper.java:38)

10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:72)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.health.DaemonHealthTracker.execute(DaemonHealthTracker.java:47)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:60)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:72)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.health.HintGCAfterBuild.execute(HintGCAfterBuild.java:41)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:50)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:237)

10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] Caused by: java.lang.ArrayIndexOutOfBoundsException: -1

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at com.sun.xml.internal.bind.v2.util.CollisionCheckStack.pushNocheck(CollisionCheckStack.java:117)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:472)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:308)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:236)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at android.databinding.tool.store.ResourceBundle$LayoutFileBundle.toXML(ResourceBundle.java:629)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at android.databinding.tool.LayoutXmlProcessor.writeXmlFile(LayoutXmlProcessor.java:252)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at android.databinding.tool.LayoutXmlProcessor.writeLayoutInfoFiles(LayoutXmlProcessor.java:239)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at com.android.build.gradle.internal.tasks.databinding.DataBindingProcessLayoutsTask.processResources(DataBindingProcessLayoutsTask.java:110)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:75)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.doExecute(AnnotationProcessingTaskFactory.java:245)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:221)

10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.execute(AnnotationProcessingTaskFactory.java:232)

10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:210)

10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)

10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter] at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)

10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter] ... 68 more

10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter]

 

经过分析和猜测后,发现每次都是同一个 module 堵住的,进去看了看…竟然几乎是空的,是个还没有进行组件化重构的模块(只有一个 manifest 和 string.xml),然而 build.gradle 却使用了 data binding。看来又是个 Google 埋下的坑。心很累,就不去报 bug 了。

Dagger2

几个月前写过从零开始的Android新项目4 - Dagger2篇 ,用了快一年时间的 Dagger2 后,越来越觉得这种注入方式很不错。

然而没想到在组件化改造中会这么坑,但是也不能怪 Dagger2,而是原先隔离就做的不够好。

从设计上来说,Component 和独有的 Module 都只能放在对应的业务 module 中。module 之间不能互相访问彼此的 Dagger Module。且 data 和 domain 两个 module 中各种业务独有的类也应该放在业务 module 中,或者至少应该分拆出来。否则在 Module A 进行组件化开发的时候,却能引用 Module B 的 Api 类以及数据 Bean,简单来说也就是知道得太多。

所以如果使用了 Dagger2,这里就需要把原来的 scope 更进一步做到极致,理清所有依赖的可见区域。

最佳实践

每个 module 包名都应该使用 “$packageName.module.$business” 形式,资源使用业务名开头,比如 “feed_ic_like.png”。

另外,在组件化实践过程中可能碰到的就是依赖的问题了,然而因为我们项目本身就设计得还算不错,所以并没有在这方面需要做任何修改,整个项目的架构图如下:

从零开始的Android新项目之组件化实践

简化了不少,有些省略了,因为实在懒得画。对模块来说,通用的东西放在底层 library(utils、widget),而只有自己用的则放在自己 module 就行了。

作为一个善意提醒,如果一个模块分拆为三个模块,那 clean build 的速度肯定会变慢,要有心理准备。

模块隔离

可参考上图,关键的点就是高内聚,低耦合。

通用的东西按照其功能性划分在不同 library 模块中。见上图(已经省略了不少了,实际 module 更多一些)。

改进点在于,从组件化角度来讲,data 和 domain 并不是一个 public 的 scope,也应该放在各个业务模块中,但因为目前的实现,进行重构代价太大,只能放在以后新模块进行实践。

RPC

RPC 在广义上指的是一种通信协议,允许运行于一台计算机的程序调用另一台计算机的子程序,而开发者无需额外地为这个交互作用编程。Android 上的 AIDL 也是一种 RPC 的实现。

这里指的 RPC 并没有跨进程或者机器,而是一种类似的 —— 在彼此无法互相访问的时候的接口定义和调用。

Proxy

通用的 Proxy 抽象类:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

 

public abstract class Proxy<T, C> implements IProxy<T, C> {

private static final String TAG = "Proxy";

private Module<T, C> proxy;

@Override

public final T getUiInterface() {

return getProxy().getUiInterface();

}

@Override

public final C getServiceInterface() {

return getProxy().getServiceInterface();

}

public abstract String getModuleClassName();

public abstract Module<T, C> getDefaultModule();

protected Module<T, C> getProxy() {

if (proxy == null) {

String module = getModuleClassName();

if (!TextUtils.isEmpty(module)) {

try {

proxy = (Module<T, C>) ModuleManager.LoadModule(module);

} catch (Throwable e) {

LogUtils.e(TAG, module + " module load failed", e);

proxy = getDefaultModule();

}

}

}

return proxy;

}

}

 

实现类则集成并重载两个抽象方法:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

public class FeedProxy extends Proxy<IFeedUI, IFeedService> {

public static final FeedProxy g = new FeedProxy();

// 在没有获得真实实现时候的默认实现

@Override

public Module<IFeedUI, IFeedService> getDefaultModule() {

return new DefaultFeedModule();

}

// 真实实现的类

@Override

public String getModuleClassName() {

return "com.amokie.stay.module.feed.FeedModule";

}

}

 

IFeedUI 定义 Feed 模块中的 UI 相关接口,IFeedService 则是 Feed 模块的服务接口。

建议直接暴露 intent 或者 void 方法来提供跳转,而不是返回 activity。

Router

最 low 的就是用 Class.forName 去拿 activity 或者 fragment 了…其他可以使用 scheme、各自注册、甚至类 RPC 的调用方式。

为什么说 forClass 去获取 activity 或者 fragment 很 low ?模块 A 想去模块 B 的一个页面,拿到 activity 后,难道还要自己去填 intent,还要自己去问人到底需要哪些参数,需要以什么形式过去?再者如果是要去模块 B 的某个 activity 中的某个 fragment,怎么表示?

性能问题就不谈了。这么定义后,以后包名类名都不敢换了。

RPC

就是上面提到的类似 IFeedUI 这样的类了,使用的时候

 

1

 

FeedProxy.g.getUiInterface().goToUserHome(context, userId);

根据灵活性和需要,也可以把 intent 本身作为初始参数传入。

注册

即每个页面自行去* Navigator 注册自己的 Url。

* Navigator 维护一个 Hashmap 用于查询跳转。

如此,我们就依然可以通过 Android 原生的 Bundle/Intent 来传 Parcelable 数据。

scheme

Android 原生的 scheme。当我们在浏览器或者一个应用呼起另一个应用,使用的就是这个机制。

与上一个方法不同的是,这是 Android 原生支持的,我们需要在 manifest 进行注册:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

<activity

android:name="com.amokie.stay.module.card.ReactCardDetailActivity"

android:screenOrientation="portrait">

<intent-filter>

<action android:name="android.intent.action.VIEW"/>

<category android:name="android.intent.category.DEFAULT"/>

<category android:name="android.intent.category.BROWSABLE"/>

<data

android:host="card"

android:scheme="stayapp"/>

</intent-filter>

</activity>

 

跳转调用更简单:

 

1

 

intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));

 

参数可以使用类似 url param 的形式,比如:stayapp://feed-detail/?id=1234&guest=true。
简单情况下也能直接使用 Rest 形式,即 stayapp://feed-detail/1234,但如此就只能传递一个数据过去了,毕竟 Rest 是一种资源描述。

Software -> Peopleware,在项目逐渐变大后,团队人数变大,需求复杂度上升,组件化的开发形式可以隔绝模块间耦合,降低中大型团队的开发成本,而且编译速度也能提升(独立模块编译运行)。

转载自:http://www.apkbus.com/blog-865069-78923.html