前言

众所周知,黄油刀 ButterKnife 已经抛弃了,而且已经不再保护了,而一些老项目估量还有一堆这样的代码,信任咱们多多少少都有过被 @BindView 或许 @OnClick 分配的恐惧,而假如想要一个页面一个页面的移除的话,工作量也是非常大的,而这也是笔者写这个插件的原因了(这儿不解说插件开发的相关常识,插件开发运用的是 IntelliJ IDEA)。

注:由于每个项目的封装的多样性、以及 layout 布局的初始化有各种各样的写法,还有涉及到一些语法语义的联系,代码无法做到精准转化(后边会举一些比如),所以插件无法做到百分百转化成功,在转化后建议手动查看一下是否犯错。

本文关于没有插件开发以及 PSI 基础的人或许会看不下去,能够直接 github传送门 跳 github 链接并 clone 代码运转,一键完结 ButterKnife 的移除并替换成 ViewBinding 。

支撑的言语与类

目前仅支撑 Java 言语,由于信任假如项目中运用的是 Kotlin ,那必定首选 KAE 或许 ViewBinding 了(优选 ViewBinding ,现在 KAE 也已经被移除了)。

该插件中目前对不同的类有不同的转化办法

  • Activity、Fragment、自定义 View 是移除 ButterKnife 并转化成 ViewBinding
  • ViewHolder、Dialog 是移除 ButterKnife 并转化成 findViewById 办法

由于 Activity 与 Fragment 关于布局的塞入是比较一致的,所以能够做到比较精准的转化为 ViewBinding,而自定义 View 虽然布局的写法也林林总总,可是笔者也尽量修正一致了,而 ViewHolder 与 Dialog 比较复杂,直接修正成 findViewById 比较不简单犯错(假如对自己的项目写法的一致很有信心的,也能够按照自己项目的写法试着修正一下代码,都改成 ViewBinding 会更好),究竟谁也不希望修正后的代码一团糟是吧~

思路解说

研究代码

首先咱们需求研究一下运用了 ButterKnife 的代码是怎样样的,假如是自己运用过该插件的同学必定是很了解、它的写法的,而关于笔者这种没运用过,可是公司的老项目中 java 的部分满是运用了 ButterKnife 的就很难受了,然后列出咱们需求关心的注解。

  • @BindView:用于符号 xml 里的各种特点

  • @OnClick:用于符号 xml 中特点对应的点击事情

  • @OnLongClick:用于符号 xml 中特点对应的长按事情

  • @OnTouch:用于符号 xml 中特点对应的 touch 事情

这儿不做过多解说,究竟又不是教咱们怎样用 ButterKnife 是吧~

捋清思路

上面提到的相关注解是咱们需求移除的,咱们要针对咱们转化的不同办法对这些注解符号的变量与办法做不同的操作。

  • 关于修正成 findViewById 办法的类,咱们只需求记录下来该注解以及注解对应的变量或许办法称号,然后新增 initView() 办法用于初始化记录下来的变量,新增 initListener() 办法用于点击事情的编写。

  • 关于修正成 ViewBinding 办法的类,咱们不只需求记录该注解与对应的变量和办法,而且还需求遍历类中的全部代码,在检索到该符号的变量后,需求把这些变量都修正成 mBinding.xxx 的办法,留意:一般咱们xml的id命名喜爱用_下划线,可是ViewBinding运用的运用是需求主动改成驼峰式命名的

除此之外,咱们需求移除的还有 ButterKnife 的 import 句子绑定句子 bind()、以及解绑句子 unbind()。咱们需求增加的有:layout 对应的 ViewBinding 类的初始化句子、import 句子。

了解完这些咱们就能够开端写插件啦~

代码编写

关于代码的编写笔者这儿也会分几个过程去阐述:别离是 PSI 相关常识、文件处理、编写举例、留意事项。

PSI相关常识

PSI 的全称是 Program Structure Interface(程序结构接口),咱们要剖析代码以及修正代码的话,是离不开 PSI 的,文档传送门

一个 Class 文件结构别离包括字段表、特点表、办法表等,每个字段、办法也都有特点表,但在 PSI 中,总体上只要 PsiFilePsiElement

  • PsiFile 是一个接口,假如文件是一个 java 文件,那么解析生成的 PsiFile 便是 PsiJavaFile 目标,假如是一个 Xml 文件,则解析后生成的是 XmlFile 目标

  • 而对应 Java 文件的 PsiElement 品种有:PsiClass、PsiField、PsiMethod、PsiCodeBlock、PsiStatement、PsiMethodCallExpression 等等

其间,PsiJavaFile、PsiClass、PsiField、PsiMethod、PsiStatement 是咱们本文涉及到的,咱们能够先去看看文档了解一下。

文件处理

咱们在挑选多级目录的时分,会有许多的文件,而咱们需求在这些文件中挑选出 java 文件,以及挑选出 import 句子中含有 butterknife 的,由于假如该类运用了 ButterKnife ,则必定需求 import 相关的类。

挑选 java 文件的这部分代码在这儿就不贴出来了,很简单的,咱们能够直接去看代码就好。

判别该类是否需求进行 ButterKnife 移除处理:

/**
 * 查看是否有import butterknife相关,若没有引进butterknife,则不需求操作
 */
private fun checkIsNeedModify(): Boolean {
    val importStatement = psiJavaFile.importList?.importStatements?.find {
        it.qualifiedName?.lowercase(Locale.getDefault())?.contains("butterknife") == true
    }
    return importStatement != null
}

在这儿需求先来一些前置常识,咱们的插件在获取文件的的时分,拿到的是 VirtualFile,当该文件是java文件时,VirtualFile 能够经过 PSI 供给的api转化成 PsiJavaFile,然后咱们能够经过 PsiFile 拿到 PsiClass,其间,importList 是归于 PsiFile 的,而上面提到那些 PsiElement 都是归于 PsiClass 的。

下面贴一下这部分代码:

private fun handle(vFile: VirtualFile) {
    if (vFile.isDirectory) {
        handleDirectory(vFile)
    } else {
        // 判别是否是java类型
        if (vFile.fileType is JavaFileType) {
            // 转化成psiFile
            val psiFile = PsiManager.getInstance(project!!).findFile(vFile)
            // 转化成psiClass
            val psiClass = PsiTreeUtil.findChildOfAnyType(psiFile, PsiClass::class.java)
            handleSingleVirtualFile(vFile, psiFile, psiClass)
        }
    }
}

这儿只需求了解的便是增加了注释的那几行代码。

编写举例

咱们需求对 PsiClass 进行分类,这儿目前是只能按照大部分人对类的命名习气来进行剖析,假如有一些特殊的命名习气的人,能够把代码 clone 下来自行修正一下再运转。

private fun checkClassType(psiClass: PsiClass) {
    val superType = psiClass.superClassType.toString()
    if (superType.contains("Activity")) {
        ActivityCodeParser(project, vFile, psiJavaFile, psiClass).execute()
    } else if (superType.contains("Fragment")) {
        FragmentCodeParser(project, vFile, psiJavaFile, psiClass).execute()
    } else if (superType.contains("ViewHolder") || superType.contains("Adapter<ViewHolder>")) {
        AdapterCodeParser(project, psiJavaFile, psiClass).execute()
    } else if (superType.contains("Adapter")) {
        // 这儿的判别是为了不做处理,由于adapter的xml特点是在viewHolder中初始化的
    } else if (superType.contains("Dialog")) {
        DialogCodeParser(project, psiJavaFile, psiClass).execute()
    } else { 
        // 自定义View
        CustomViewCodeParser(project, vFile, psiJavaFile, psiClass).execute()
    }
}

咱们经过拿到 PsiClass 承继的父类的类型来进行判别,这儿的不足是代码中只拿了当时类的上一级承继的父类的类型,并没有去判别父类是否还有父类,由于笔者认为只要命名规范,这就不是什么大问题。举个比如,假如有人喜爱封装一个名为 BaseFragment 的实则是一个 Activity 的基类,然后由 MainActivity 去承继,那这个插件就不适用了

这儿要留意的是,咱们此刻仅仅判别了外部类,而一个 class 中或许会有多个内部类,如 Adapter 中的 ViewHolder 便是一个很好的比如了,所以咱们还需求遍历每一个 class 中的 innerClass,然后进行相同的操作:

// 内部类处理
psiClass.innerClasses.forEach {
    checkClassType(it)
}

由于涉及到的类别太多,所以这儿只挑两个比如出来解说,别离是 ButterKnife 转化为 ViewBinding 的 Activity、ButterKnife 转化为 findViewById 的 ViewHolder,由于涉及到运用 PSI 剖析并修正代码,为了便利一致剖析管理,所以这儿抽了个基类。

下面先来看一下基类中两个比较重要的办法,了解了这两个办法后边的代码才更简单了解: BaseCodeParser

private val bindViewFieldLists = mutableListOf<Pair<String, String>>() // 运用@BindView的特点与单个字段
private val bindViewListFieldLists = mutableListOf<Triple<String, String, MutableList<String>>>() // 运用@BindView的特点与多个字段
protected val innerBindViewFieldLists = mutableListOf<Pair<String, String>>() // 需求运用fvb办法的类 -- @BindView的特点与单个字段
/**
 * 遍历一切字段并找到@BindView注解
 * @param isDelete 是否删去@BindView注解的字段 true -> 删去字段  false -> 仅删去注解
 */
fun findBindViewAnnotation(isDelete: Boolean = true) {
    psiClass.fields.forEach {
        it.annotations.forEach { psiAnnotation ->
            // 找到了@BindView注解
            if (psiAnnotation.qualifiedName?.contains("BindView") == true) {
                // 判别该注解中的value个数,若为多个,则用另外的办法记录处理
                if ((psiAnnotation.findAttributeValue("value")?.text?.getAnnotationIds()?.size ?: 0) > 1) {
                    val first = it.name
                    val second = mutableListOf<String>()
                    psiAnnotation.findAttributeValue("value")?.text?.getAnnotationIds()?.forEach { id ->
                        second.add(id)
                    }
                    bindViewListFieldLists.add(Triple(it.type.toString(), first, second))
                    writeAction{
                        // 只删去注解,不删去字段
                        psiAnnotation.delete()
                    }
                } else {
                    // 否则直接记录注解符号的变量称号与注解中的value,也便是xml中的id
                    val first = it.name
                    val second = psiAnnotation.findAttributeValue("value")?.lastChild?.text.toString()
                    if (isDelete) {
                        bindViewFieldLists.add(Pair(first, second))
                    } else {
                        innerBindViewFieldLists.add(Pair(first, second))
                    }
                    writeAction {
                        if (isDelete) {
                            it.delete()
                        } else {
                            psiAnnotation.delete()
                        }
                    }
                }
            }
        }
    }
}
/**
 * 遍历一切办法并找到@OnClick / @OnLongClick / @OnTouch注解
 */
fun findOnClickAnnotation() {
    psiClass.methods.forEach {
        it.annotations.forEach { psiAnnotation ->
            // 找到了被@OnClick或@OnLongClick或@OnTouch符号的办法
            if (psiAnnotation.qualifiedName?.contains("OnClick") == true || psiAnnotation.qualifiedName?.contains("OnLongClick")
                == true || psiAnnotation.qualifiedName?.contains("OnTouch") == true) {
               // 遍历该注解中的一切value并保存
               psiAnnotation.findAttributeValue("value")?.text?.getAnnotationIds()?.forEach { id ->
                    var second = "${it.name}("
                    // 获取该办法中的一切参数,跟办法名一同拼接起来,便利后边直接调用
                    it.parameterList.parameters.forEachIndexed { index, params ->
                        // 为了适配各种不同的命名,所以这儿运用一致的命名
                        // 由于这三个注解只会存在这几个类型的参数
                        if (params.type.toString() == "PsiType:View") {
                            second += "view"
                        } else if (params.type.toString() == "PsiType:MotionEvent") {
                            second += "event"
                        }
                        if (index != it.parameterList.parameters.size - 1) {
                            second += ", "
                        }
                    }
                    second += ")"
                    if (psiAnnotation.qualifiedName?.contains("OnClick") == true) {
                        onClickMethodLists.add(Pair(id, second))
                    } else if (psiAnnotation.qualifiedName?.contains("OnLongClick") == true) {
                        onLongClickMethodLists.add(Pair(id, second))
                    } else if (psiAnnotation.qualifiedName?.contains("OnTouch") == true) {
                        onTouchMethodLists.add(Pair(id, second))
                    }
                }
                writeAction {
                    // 删去@OnClick注解
                    psiAnnotation.delete()
                }
            }
        }
    }
}
/**
 * 代码写入,修正的代码一致运用该办法进行修正写入
 */
private fun writeAction(commandName: String = "RemoveButterKnifeWriteAction", runnable: Runnable) {
    WriteCommandAction.runWriteCommandAction(project, commandName, "RemoveButterKnifeGroupID", runnable, psiJavaFile)
}

这儿的代码或许会让人有点懵,下面来解说一下这些代码,先解说第一个办法:该办法是保存一切运用了 @BindView 注解符号的变量,能够看到代码中是分了 if else 去处理的,原因是有些代码的 @BindView 中的 value 只要一个,有些的会有多个,多个 value 的场景一般是运用 List 或许数组 Object[] 来进行修饰的,如下比如:

拯救旧项目,一键移除ButterKnife并替换为ViewBinding

假如注解中只要单个 value,咱们是能够直接改成 mBindind.xxx,而假如是 List 或许数组的办法的话,咱们需求另外处理,这儿笔者**运用的办法是记录一个变量若对应多个 xml 特点,则把这些特点都增加进该变量中,如 mTabViews.add(mBinding.xxx) **,要保证不影响原本的运用办法。

而第二个办法是保存一切运用了 @OnClick、@OnLongClick、@OnTouch 符号的办法,同上,多个特点的点击事情或许会是同一个办法,如下比如:

拯救旧项目,一键移除ButterKnife并替换为ViewBinding

看完了基类的两个重要办法,下面咱们来看一下关于咱们的 Activity 要怎样转化:

ActivityCodeParser

class ActivityCodeParser(
    project: Project,
    private val vFile: VirtualFile,
    psiJavaFile: PsiJavaFile,
    private val psiClass: PsiClass
) : BaseCodeParser(project, psiJavaFile, psiClass) {
    init {
        findBindViewAnnotation()
        findOnClickAnnotation()
    }
    override fun findViewInsertAnchor() {
        // 找到onCreate办法
        val onCreateMethod = psiClass.findMethodsByName("onCreate", false)[0]
        onCreateMethod.body?.statements?.forEach { statement ->
            // 判别布局在哪个statement中,并拿到R.layout.后边的姓名
            if (statement.text.trim().contains("R.layout.")) {
                val layoutRes = statement.text.trim().getLayoutRes()
                // 把布局称号转化成Binding实例称号。如activity_record_detail -> ActivityRecordDetailBinding
                val bindingName = layoutRes.underLineToHump().withViewBinding()
                val afterStatement = elementFactory.createStatementFromText(statement.text.toString().replace("R.layout.$layoutRes", "mBinding.getRoot()"), psiClass)
                // 以下四个办法都在基类BaseCodeParser中,后边再解说
                addBindingField("private $bindingName mBinding = $bindingName.inflate(getLayoutInflater());\n")
                addBindViewListStatement(onCreateMethod, statement)
                changeBindingStatement(onCreateMethod, statement, afterStatement)
                addImportStatement(vFile, layoutRes)
            }
        }
        // 遍历Activity中的一切办法并遍历办法中的一切statement
        psiClass.methods.forEach {
            it.body?.statements?.forEach { statement ->
                // 把一切原本运用@BindView符号的变量改为mBinding.xxx
                changeBindViewStatement(statement)
            }
        }
        // 内部类也或许运用外部类的变量
        psiClass.innerClasses.forEach {
            it.methods.forEach { method ->
                method.body?.statements?.forEach { statement ->
                    changeBindViewStatement(statement)
                }
            }
        }
    }
    override fun findClickInsertAnchor() {
        // 在onCreate中增加initListener办法,并把保存下来的监听事情写入该办法中
        val onCreateMethod = psiClass.findMethodsByName("onCreate", false)[0]
        insertOnClickMethod(onCreateMethod)
    }
}

关于咱们的 Activity,思路便是先找到 OnCreate() 办法,众所周知,Activity 的 layout 布局是写在 onCreate 中的 setContentView() 中的,所以咱们需求找到这句 statement,拿到布局称号,再转化为驼峰式 + 首字母大写,并在后边加上 Binding,这便是 ViewBinding 给咱们布局生成的类称号,不多做解说,了解运用 ViewBinding 的人都会清楚的。

这儿需求留意的是,上面的写法仅仅惯例的 layout 布局写法,还有一些项目喜爱自行封装的,比如喜爱把布局称号写在 getLayoutId() 中,然后在基类一致写成 setContentView(getLayoutId())。运用这种写法或许是其他封装办法的童鞋能够自行修正一下代码再运转,由于封装的办法太多了,这儿无法做适配。

现在再来看一下上面未做解说的几个办法,首先来看一下 addBindingField() ,这是一个给class增加字段的办法:

val elementFactory = JavaPsiFacade.getInstance(project).elementFactory
/**
 * 增加mBinding变量
 */
protected fun addBindingField(fieldStr: String) {
    psiClass.addAfter(elementFactory.createFieldFromText(fieldStr, psiClass), psiClass.allFields.last())
}

elementFactory 是一个 PsiElementFactory 目标,用于创立 PsiElement,也便是上面所介绍的各种 PsiElement 。这儿咱们需求先创立一个 mBinding 变量,关于 Activity 咱们能够直接经过 private bindingName mBinding = bindingName.inflate(getLayoutInflater()); 去实例化 mBinding 。

下面来看一下 addBindViewListStatement()

/**
 * 为运用这种办法的@BindViews({R.id.layout_tab_equipment, R.id.layout_tab_community, R.id.layout_tab_home})增加list
 */
protected fun addBindViewListStatement(psiMethod: PsiMethod, psiStatement: PsiStatement) {
    bindViewListFieldLists.forEachIndexed { index, triple ->
        writeAction {
            if (triple.first.contains("PsiType:List")) {
                psiMethod.addAfter(elementFactory.createStatementFromText("${triple.second} = new ArrayList<>();\n", psiClass), psiStatement)
            } else {
                psiMethod.addAfter(elementFactory.createStatementFromText("${triple.second} = new ${triple.first.substring(8, triple.first.length - 1)}${triple.third.size}];\n", psiClass), psiStatement)
            }
            psiMethod.body?.statements?.forEach { statement ->
                // 初始化变量并增加保存下来的一切xml特点
                if (statement.text.trim() == "${triple.second} = new ArrayList<>();" || statement.text.trim() == "${triple.second} = new ${triple.first.substring(8, triple.first.length - 1)}${triple.third.size}];") {
                    triple.third.asReversed().forEachIndexed { index, name ->
                        if (triple.first.contains("PsiType:List")) {
                            psiClass.addAfter(elementFactory.createStatementFromText("${triple.second}.add(mBinding.${name.underLineToHump()});\n", psiClass), statement)
                        } else {
                            psiClass.addAfter(elementFactory.createStatementFromText("${triple.second}[${triple.third.size - 1 - index}] = mBinding.${name.underLineToHump()};\n", psiClass), statement)
                        }
                    }
                }
            }
        }
    }
}

上面的注释解说得很清楚,咱们的 @BindView 或许会引证许多个 xml 特点,而该注解符号的字段或许是 List 也或许是数组,所以咱们需求先判别该字段是归于哪品种型,并进行初始化。这儿需求留意的是:在遍历增加字段的时分需求逆序增加,由于咱们在增加一句 statement 的时分只要一个唯一参照物便是 new ArrayList<>() 或许是 new Objetc[] ,咱们新增加的 statement 只能在这句代码后边增加,所以实际上增加完后的代码次序是倒过来的,需求逆序。

接下来看一下 changeBindingStatement() 办法:

/**
 * 修正mBinding的初始化句子
 * @param method 需求修正的句子地点的办法
 * @param beforeStatement 修正前的句子
 * @param afterStatement 修正后的句子
 */
protected fun changeBindingStatement(method: PsiMethod, beforeStatement: PsiStatement, afterStatement: PsiStatement) {
    writeAction {
        method.addAfter(afterStatement, beforeStatement)
        beforeStatement.delete()
    }
}

这个办法没什么好说的,结合上面的运用,便是把原本的 setContentView(R.layout.xxx) 改成 setContentView(mBinding.getRoot()) 而已。

最终再来看一下 addImportStatement() 办法,这个办法是最复杂的,众所周知,咱们在运用 ViewBinding 主动生成的类时需求导包,可是这个包的途径怎样才能得到呢?由于咱们一个项目中必定会有多个 module 以及多个目录,咱们无法确认当时处理的文件所属的是哪个 module ,也无法确认当时 module 中运用的 xml 文件是否是其他 module 的(究竟 xml 文件是能够跨 module 运用的),由于不确认性太多导致无法正确拿到该 Binding 类的包名途径进行导包,所以咱们需求采纳其他办法。

咱们都知道在敞开 ViewBinding 的开关的时分,咱们每个 xml 都会主动生成对应的 Binding 类,坐落 build/generated/data_binding_base_class_source_out/debug/out 目录中,这儿咱们仅仅带过,咱们真实需求的文件不在这儿,咱们真实需求拿的是每个 Binding 类与所在的包名途径的映射文件,坐落 build/intermediates/data_binding_base_class_log_artifact/debug/out 中的一个 json 文件,如下图所示:

拯救旧项目,一键移除ButterKnife并替换为ViewBinding

而这个 json 文件只要在项目编译过后才会生成,咱们也能够经过履行 task 去生成该文件,具体过程后边会给出。

咱们只需求解析这个 json 文件,然后经过上面拿到的 Binding 称号,再去拿对应的 module_package ,就能拿到当时的 Binding 类的途径了,最终再经过 import 句子直接导包就好了。思路给了,由于代码太长篇幅有限,有爱好的能够直接去看代码~

接下来咱们来看一下怎样把原本运用 @BindView 符号的字段一致改成 mBinding.xxx 办法:

changeBindViewStatement

/**
 * 把原本运用@BindView的特点修正为mBinding.xxx
 * @param psiStatement 需求修正的statement
 */
protected fun changeBindViewStatement(psiStatement: PsiStatement) {
    var replaceText = psiStatement.text.trim()
    bindViewFieldLists.forEachIndexed { index, pair ->
        if (replaceText.isOnlyContainsTarget(pair.first) && !replaceText.isOnlyContainsTarget("R.id.${pair.first}")) {
            replaceText = replaceText.replace("\\b${pair.first}\\b".toRegex(), "mBinding.${pair.second.underLineToHump()}")
        }
        if (index == bindViewFieldLists.size - 1) {
            if (replaceText != psiStatement.text.trim()) {
                val replaceStatement = elementFactory.createStatementFromText(replaceText, psiClass)
                writeAction {
                    psiStatement.addAfter(replaceStatement, psiStatement)
                    psiStatement.delete()
                }
            }
        }
    }
}

当咱们匹配到咱们记录下来的字段以及对应的 xml 特点时,咱们就把匹配到的 statement 中含有该匹配值的当地替换成 mBinding.xxx ,这儿需求留意的是:要考虑类似的单词,如咱们要匹配的是 view ,这时假如 statement 中含有 viewModel ,咱们不能对它进行处理,所以笔者这儿用到了正则去判别,关于项目中用到的一些办法都封装在 StringExpand 中,有爱好的能够自行查看。

原本还想示例说明一下怎样增加监听事情的,可是由于篇幅太长了,这儿就不贴代码说明晰,待会直接进传送门看吧~

好了,说完了 Activity 的处理,现在咱们来看一下关于转化为 findViewById 的 ViewHolder 咱们怎样处理吧~

class AdapterCodeParser(project: Project, psiJavaFile: PsiJavaFile, private val psiClass: PsiClass) : BaseCodeParser(project, psiJavaFile, psiClass) {
    init {
        findBindViewAnnotation(false)
        findOnClickAnnotation()
    }
    private var resultMethod: PsiMethod? = null
    private var resultStatement: PsiStatement? = null
    override fun findViewInsertAnchor() {
        findMethodByButterKnifeBind()
        val parameterName = findMethodParameterName()
        resultMethod?.let {
            innerBindViewFieldLists.forEach { pair ->
                resultStatement?.let { statement ->
                    if (parameterName.isNotEmpty()) {
                        addMethodStatement(it, statement, elementFactory.createStatementFromText("${pair.first} = $parameterName.findViewById(R.id.${pair.second});", psiClass))
                    } else {
                        addMethodStatement(it, statement, elementFactory.createStatementFromText("${pair.first} = itemView.findViewById(R.id.${pair.second});", psiClass))
                    }
                }
            }
        }
    }
    /**
     * 找到ViewHolder结构函数的参数称号
     */
    private fun findMethodParameterName(): String {
        var parameterName = ""
        resultMethod?.let {
            it.parameterList.parameters.forEach { parameter ->
                if (parameter.type.toString() == "PsiType:View") {
                    parameterName = parameter.name
                    return@forEach
                }
            }
        }
        return parameterName
    }
    /**
     * 找到ButterKnife.bind的绑定句子地点的办法
     */
    private fun findMethodByButterKnifeBind() {
        run jump@{
            psiClass.methods.forEach { method ->
                method.body?.statements?.forEach { statement ->
                    if (statement.text.trim().contains("ButterKnife.bind(")) {
                        if (method.isConstructor) {
                            resultMethod = method
                            resultStatement = statement
                            return@jump
                        }
                    }
                }
            }
        }
    }
    override fun findClickInsertAnchor() {
        val parameterName = findMethodParameterName()
        resultMethod?.let {
            if (parameterName.isNotEmpty()) {
                insertOnClickStatementByFVB(it, parameterName)
            } else {
                insertOnClickStatementByFVB(it, "itemView")
            }
        }
    }
}

咱们首先是要找到 ViewHolder 中的 ButterKnife.bind 的绑定句子所在的方位,一般是处于结构函数中,然后咱们需求拿到结构函数中参数类型为 View 的参数称号,由于有些人喜爱命名为 view ,有些人喜爱命名为 itemView ,所以咱们要拿到参数称号后才能够增加 findViewById 句子,如 text = itemView.findViewById(R.id.text) ,这儿还有一种其他状况便是结构函数里或许没有参数类型为 View 的参数,这时咱们只需求一致运用 itemView 就能够了。

ViewHolder 的转化很简单,该解说的办法上面也解说了,没解提到的只能怪笔者太懒了,懒得贴那么多代码哈哈哈~

到这儿咱们已经看完了 ButterKnife 别离转化为 ViewBinding 、 findViewById 这两种办法的代表类了,最终需求留意的是咱们要修正并删去完 ButterKnife 相关注解的时分,也要把相关的 ButterKnife.bind() 句子以及 import 句子删掉

/**
 * 删去ButterKnife的import句子、绑定句子、解绑句子
 */
private fun deleteButterKnifeBindStatement() {
    writeAction {
        psiJavaFile.importList?.importStatements?.forEach {
            if (it.qualifiedName?.lowercase()?.contains("butterknife") == true) {
                it.delete()
            }
        }
        psiClass.methods.forEach {
            it.body?.statements?.forEach { statement ->
                if (statement.text.trim().contains("ButterKnife.bind(")) {
                    statement.delete()
                }
            }
        }
        val unBinderField = psiClass.fields.find {
            it.type.canonicalText.contains("Unbinder")
        }
        if (unBinderField != null) {
            psiClass.methods.forEach {
                it.body?.statements?.forEach { statement ->
                    if (statement.firstChild.text.trim().contains(unBinderField.name)) {
                        statement.delete()
                    }
                }
            }
            unBinderField.delete()
        }
    }
}

留意事项

在前言提到的涉及到一些语法语义的联系,代码无法做到精准转化的时分说了后边会举例说明,这儿举几个常见的比如:

  • 相关回调的参数称号与 xml 中的特点称号相同
@BindView(R.id.appBar)
AppBarLayout appBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    appBar.addOnOffsetChangedListener((appBar, verticalOffset) -> {
        ...
    });
}

能够看到这儿有两个 appBar ,一个是上面 @BindView 符号的 appBar ,另一个是回调监听中的参数,所以这儿会不可避免的把两个 appBar 都修正成 mBinding.xxx ,可是在修正回调参数的 appBar 时,这个类会报错,所以后边在查看犯错的类时会看到这个错误。这种状况能够经过修正回调参数的称号处理,修正之后再从头履行一次就能够了。

  • @BindView 符号的字段是 layout 中某个自定义 View 里的 xml 特点

这个就不贴代码举比如了,总的来说便是假设 MainActivity 中的布局是 activity_main ,该布局中含有一个 CustomView ,而 CustomView 中有一个布局 layout_custom_view ,而 layout_custom_view 中有一个 TextView 的 id 是 tv_content ,而这个 tv_content 是能够经过 ButterKnife 直接在 MainActivity 中运用的,可是修正成 ViewBinding 之后是拿不到这个 mBinding.tvContent 的(不知道我这么说咱们能不能了解)

  • Activity 中经过 if else 判别 setContentView 需求塞入哪个布局
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    if (xxx > 0) {
        setContentView(R.layout.layout1);
    } else {
        setContentView(R.layout.layout2);
    }
 }

这种状况真的是不知道该实例化哪个 Binding 类,仍是老老实实的手动修正成 findViewById 吧。

运用过程

  • 在项目中敞开 ViewBinding
android {
        viewBinding {
            enabled = true
        }
    }
  • 生成 ViewBinding 相关的类

在项目目录下履行 ./gradlew dataBindingGenBaseClassesDebug 生成 ViewBinding 相关的类与映射文件

  • 履行代码转化

右键需求转化的文件目录(支撑单个文件操作或多级目录操作),点击 RemoveButterKnife 开端转化,假如文件许多的话需求等候的时分会久一点。

  • 等候履行成果

成果如下所示,有异常的文件能够手动查看并自行处理。

拯救旧项目,一键移除ButterKnife并替换为ViewBinding

留意:转化完之后必定必定必定要查看一遍,最好打包让测试也从头测一遍!!!

github传送门