ButterKnife已经成为众多开发者的选择。简直不要太好用。
其优势
- 1、强大的Resource绑定和Method事件处理功能,简化代码,提升开发效率
- 2、方便的处理Adapter里的ViewHolder绑定问题
- 3、运行时不会影响APP效率,使用配置方便
- 4、代码清晰,可读性强
截止 2019年9月6日。ButterKnife Version 10.1.0 (2019-02-13),已经有很多的支持了。
为什么有R2.java
原因
Library Module必须转换成final类型才能使用ButterKnife。
不是常量会提示attribute value must be constant
描述
ButterKnife是通过注解 简化编码工作。
Module | 是否生成final的ID |
---|---|
App Module | 是 |
Library Module | 否 |
具体的路径:
App Module:
app/build/generated/source/r/env1/debug/包路径/R.java 这里env1/debug是编译环境
public final class R {
public static final class anim {
public static final int a3 = 0x7f050000;
public static final int a5 = 0x7f050001;
.....
}
......
}
Library Module:
module_name/build/generated/source/r/debug/包路径/R.java
public final class R {
public static final class anim {
public static int a3 = 0x7f010001;
public static int a5 = 0x7f010002;
public static int abc_fade_in = 0x7f010003;
public static int abc_fade_out = 0x7f010004;
}
......
}
R2.java如何生成?
Jakewharton大神引入了butterknife-gradle-plugin插件,用于生成变量类型为final的R2类。
先说结论:
将R.java文件复制一份,命名为R2.java,然后将R2.java的变量都改成 public static final(其实,就是加了final)。
先看主入口:
butterknife-gradle-plugin根据Module类型的不同进行区分处理。
class ButterKnifePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.all {
when (it) {
is FeaturePlugin -> {
project.extensions[FeatureExtension::class].run {
configureR2Generation(project, featureVariants)
configureR2Generation(project, libraryVariants)
}
}
is LibraryPlugin -> {
project.extensions[LibraryExtension::class].run {
configureR2Generation(project, libraryVariants)
}
}
is AppPlugin -> {
project.extensions[AppExtension::class].run {
configureR2Generation(project, applicationVariants)
}
}
}
}
}
......
}
configureR2Generation做了什么呢?
铺垫一下
processResources定义是什么呢? 更详细的信息
将资源从其源目录复制到其目标目录,并可能对其进行处理。确保目标目录中没有过时的资源。
class ButterKnifePlugin : Plugin<Project> {
......
private fun configureR2Generation(project: Project, variants: DomainObjectSet<out BaseVariant>) {
variants.all { variant ->
val outputDir = project.buildDir.resolve(
"generated/source/r2/${variant.dirName}")
val rPackage = getPackageName(variant)
val once = AtomicBoolean()
variant.outputs.all { output ->
val processResources = output.processResources
// Though there might be multiple outputs, their R files are all the same. Thus, we only
// need to configure the task once with the R.java input and action.
if (once.compareAndSet(false, true)) {
// TODO: switch to better API once exists in AGP (https://issuetracker.google.com/118668005)
val rFile =
project.files(
when (processResources) {
is GenerateLibraryRFileTask -> processResources.textSymbolOutputFile
is LinkApplicationAndroidResourcesTask -> processResources.textSymbolOutputFile
else -> throw RuntimeException(
"Minimum supported Android Gradle Plugin is 3.1.0")
})
.builtBy(processResources)
project.tasks.create("generate${variant.name.capitalize()}R2", R2Generator::class.java) {
it.outputDir = outputDir
it.rFile = rFile
it.packageName = rPackage
it.className = "R2"
variant.registerJavaGeneratingTask(it, outputDir)
}
}
}
}
}
......
}
在项目的processResources阶段,是会获取到R.java文件的信息,在 processResources task 中获取需要生成 R2 文件所在的目录和 R 文件,传参到 R2Generator进行进行下一步操作。
R2Generator处理流程
open class R2Generator : DefaultTask() {
......
fun brewJava(
rFile: File,
outputDir: File,
packageName: String,
className: String
) {
FinalRClassBuilder(packageName, className)
.also { ResourceSymbolListReader(it).readSymbolTable(rFile) }
.build()
.writeTo(outputDir)
}
R2Generator只是封装了一层FinalRClassBuilder。
还是要看FinalRClassBuilder
这里我选取了最关键的一部分
fun addResourceField(type: String, fieldName: String, fieldValue: String) {
if (type !in SUPPORTED_TYPES) {
return
}
val fieldSpecBuilder = FieldSpec.builder(Int::class.javaPrimitiveType, fieldName)
.addModifiers(PUBLIC, STATIC, FINAL)
.initializer(fieldValue)
fieldSpecBuilder.addAnnotation(getSupportAnnotationClass(type))
val resourceType =
resourceTypes.getOrPut(type) {
TypeSpec.classBuilder(type).addModifiers(PUBLIC, STATIC, FINAL)
}
resourceType.addField(fieldSpecBuilder.build())
}
在生成R2.java的时候,所有的field都被改成了public static final。
组件化中的变量考量
ButterKnife是一种便捷的工具。什么时候用?合不合适用?需要一定的考量?如果项目小,替换成本低,可以考虑全盘替换。
针对不同的情况,分而治之
改用 Resource 绑定的方式
优势:
- 1、强大的Resource绑定和Method事件处理功能,简化代码,提升开发效率
- 2、方便的处理Adapter里的ViewHolder绑定问题
- 3、运行时不会影响APP效率,使用配置方便
- 4、代码清晰,可读性强
延续R.java的方式
在ButterKnife中,有个Utils.findRequiredViewAsType方法。
**优势:** 沿用了findViewById,不改变原有编码方式,新增支持method事件处理功能。比如@OnClick
- 不打算用ButterKnife。那就对findViewById做泛型封装,以减少代码量
protected <T extends View> T generateFindViewById(int id) { return (T) findViewById(id); }
注意:
R2只是R的副本,除了多了个final 装饰,其他都是一样的。怎么改进你的编码架构,不是没有代价的。还是需要综合的考量。
继续思考组件化中的静态变量
R.java在Library Module中,非final装修会产生什么问题呢?
答:
- 凡事规定必须使用 常量 的地方都无法使用R.java中的变量。
- 不同module之间无法保证R.java的变量是不同的,非final声明就存在资源冲突的问题,这需要额外的保证。我建议用resourcePrefix进行强制前缀解决。
详细可以查看《组件化攻略-资源合并与冲突》
所以,除了resourcePrefix强制前缀限制外,推荐还是R2.java有final有保障嘛。