Android开发中,我们常会使用一些依赖注入的框架(比如xutils)来节约我们初始化View以及View的事件的代码量。但是当我们准备在Module中使用这些东西的时候却发现R文件中的Id并不是常量,而依赖注入中的参数必须是常量值,这该如何是好?

想一想造成这个问题的根本原因是什么?id的非常量问题。如果我们能想办法将id更改为常量,问题岂不就是得到了解决吗?

想要修改id为常量,直接更改R文件是不可能了,那就只能想办法复制出一份与R文件相同的类出来,而区别只是所有的field添加final标记符。有了这个解决方案,那么就开干吧。

第一步,我们来寻找一下R文件的生成时机,也就是生成R文件的Task是哪一个。gradle中每个task都有input和output,我们在build文件中寻找R文件的位置发现在:module/build/generated/source/r/debug/packageName/R.java。

为了寻找是哪个Task生成了R文件,我们在build.gradle中加入如下代码:

afterEvaluate {

tasks.all {

it.outputs.files.each { file->

if(file.absolutePath.contains('build/generated/source/r'){

println 'generated->'+it.name

}

}

}

运行后结果如下:

generated->processDebugAndroidTestResources

generated->processDebugResources

generated->processReleaseResources

从结果可以看到gradle根据不同的场景(Debug、Release、AndroidTest)有不同的Task与之对应,至此已经找到生成R文件的Task。

第二步,上一步我们找到了Task,我们还需要解决怎么生成R文件的副本文件。

第一种方式:直接复制R文件,添加final关键字,这样的话新的文件包含了所有类型的id值。那有没有办法简单的只取R.id的值呢?

于是我们再去build结果中寻找,经过寻找,我们意外发现在build/intermediates/symbols/debug 以及build/intermediates/bundles/debug中找到了一个R.txt的文件,打开后发现是这样的

int anim abc_fade_in 0x7f050000

int anim abc_fade_out 0x7f050001

int anim abc_grow_fade_in_from_bottom 0x7f050002

int anim abc_popup_enter 0x7f050003

int anim abc_popup_exit 0x7f050004

int anim abc_shrink_fade_out_from_bottom 0x7f050005

int anim abc_slide_in_bottom 0x7f050006

int anim abc_slide_in_top 0x7f050007

int anim abc_slide_out_bottom 0x7f050008

从文件内容来看,这个文件应该是用来做所有module的R文件的merge的时候的中间文件,这却刚好方便了我们。

文件中每一行是4段内容,每段内容由空格分开分别是:

[数据类型] [值类型(子类名称)] [字段名称] [字段值]

int anim abc_slide_out_bottom 0x7f050008

public static final class anim {

public static final int abc_slide_out_bottom = 0x7f050008;

}

经过这样分析,我们可以将这个文件作为我们自己的Task的input,使用同样的方式生成另一个R文件的副本K.java。不过R.txt中还有一些是int[]类型的,这样的内容我们暂时可以跳过。于是我们有了另一种方式。

第二种方式:解析R.txt文件,摘取其中的ID类型的值,同样的方法也可以筛选其他类型的值。

第三步,自定义Task生成K.java文件。

我们先看第二种方式的实现方式。

1、在/buildSrc/src/main/groovy/packageName/中添加GenerateK.groovy文件。内容如下:

import org.gradle.api.Project

import org.gradle.api.Task

public static autoGenerateR(Project projcet, Task task) {

File inputR = task.inputs.files.files.toArray()[0]

File outDir = task.outputs.files.files.toArray()[0]

def manifestFile = projcet.android.sourceSets.main.manifest.srcFile

def packageName = new XmlParser().parse(manifestFile).attribute('package')

File file = new File(inputR, 'R.txt')

StringBuffer stringBuffer = new StringBuffer()

HashMap fieldHash = new HashMap<>()

file.readLines().each {

String[] fields = it.split(' ')

if (fields.length == 4) {

List tmpList = fieldHash.get(fields[1])

if (tmpList == null) {

tmpList = new ArrayList();

}

if (fields[1].equals('id')) {

tmpList.add('public static final ' + fields[0] + ' ' + fields[2] + ' = ' + fields[3] + ' ;')

fieldHash.put(fields[1], tmpList)

}

}

}

stringBuffer.append('package ' + packageName + ';\n')

stringBuffer.append('public final class K { \n')

fieldHash.each { k, v ->

stringBuffer.append(' public static final class ' + k + ' { \n')

v.each {

stringBuffer.append(' ' + it + '\n')

}

stringBuffer.append(' }\n')

}

stringBuffer.append('}\n')

File destFile = new File(outDir, '/' + packageName.toString().replace('.', '/') + '/K.java')

if (!destFile.parentFile.exists()) {

destFile.parentFile.mkdirs()

}

destFile.write(stringBuffer.toString(), 'utf-8')

}

2、在build.gradle中添加如下代码:

afterEvaluate {

getTasks().all { tsk ->

if (tsk.name.endsWith("Resources")

&& tsk.name.startsWith("process")

&& !tsk.name.contains('AndroidTest')) {

def buildType = tsk.name.replace("process", "").replace("Resources", "")

def taskK = task("build" + buildType + "K", dependsOn: tsk) {}

tsk.outputs.files.each {

if (it.absolutePath.contains('generated/source/r')) {

taskK.outputs.file(it.absolutePath)

}

if (it.absolutePath.contains('intermediates/symbols')

|| it.absolutePath.contains('intermediates/bundles/')) {

taskK.inputs.file(it.absolutePath)

}

}

taskK.doLast {

GenerateK.autoGenerateR(project, taskK)

}

tsk.doLast {

GenerateK.autoGenerateR(project, taskK)

}

}

}

}

3、执行buildDebugK或者buildReleaseK

现在,我们什么都准备好了,直接执行assembleDebug或者assembleRelease,或者执行buildDebugK或者buildReleaseK就都能生成K.java文件啦。文件位置在:module/build/generated/source/r/debug/packageName/K.java。

现在我们再用第一种方式实现:

1、在上一种实现方式的GenerateK.groovy文件中加入如下代码:

public static autoGenerateK(Project projcet, Task task) {

File inputR = task.inputs.files.files.toArray()[0]

File outDir = task.outputs.files.files.toArray()[0]

def manifestFile = projcet.android.sourceSets.main.manifest.srcFile

def packageName = new XmlParser().parse(manifestFile).attribute('package')

String packageDir = packageName.toString().replace('.', '/')

File rFile = new File(outDir, packageDir + '/R.java')

StringBuffer rStringBuffer = new StringBuffer();

rFile.readLines().each {

rStringBuffer.append(it + '\n')

}

String kFileContent = rStringBuffer.toString().replace('public static int', 'public static final int')

kFileContent = kFileContent.replace('public final class R', 'public final class K')

File destFile = new File(outDir, '/' + packageName.toString().replace('.', '/') + '/K.java')

if (!destFile.parentFile.exists()) {

destFile.parentFile.mkdirs()

}

destFile.write(kFileContent, 'utf-8')

}

2、修改上一种实现方式的第二步的GenerateK.autoGenerateR(project, taskK)改成GenerateK.autoGenerateK(project, taskK),然后同样的再执行第三步。

打开看看吧,然后把原来注解需要R.id的地方都替换成K.id试试,是不是满足了我们的需求呢?

其实到这里我们已经完工了,但是却不完美,因为每次id增加/删除/修改时都无法实时的在代码提示中收到反馈,需要执行一次buildXXXK这个Task(虽然很快),这个问题...有待研究,或许做一个Android Studio的插件可以达到效果~~

不过,R文件也只支持增加Id而不支持删除Id的实时反馈。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐