Android组件化:在Module中使用IOC框架

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<String, List> 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的实时反馈。

推荐阅读更多精彩内容