一鍵移除ButterKnife并替換為ViewBinding的舊項(xiàng)目拯救
前言
眾所周知,黃油刀 ButterKnife 已經(jīng)廢棄了,并且已經(jīng)不再維護(hù)了,而一些老項(xiàng)目估計(jì)還有一堆這樣的代碼,相信大家多多少少都有過被 @BindView
或者 @OnClick
支配的恐懼,而如果想要一個(gè)頁(yè)面一個(gè)頁(yè)面的移除的話,工作量也是非常大的,而這也是筆者寫這個(gè)插件的原因了(這里不講解插件開發(fā)的相關(guān)知識(shí))。
注:由于每個(gè)項(xiàng)目的封裝的多樣性、以及 layout 布局的初始化有各種各樣的寫法,還有涉及到一些語(yǔ)法語(yǔ)義的聯(lián)系,代碼無(wú)法做到精準(zhǔn)轉(zhuǎn)換(后面會(huì)舉一些例子),所以插件無(wú)法做到百分百轉(zhuǎn)換成功,在轉(zhuǎn)換后建議手動(dòng)檢查一下是否出錯(cuò)。
本文對(duì)于沒有插件開發(fā)以及 PSI 基礎(chǔ)的人可能會(huì)看不下去,可以直接 github傳送門 跳 github 鏈接并 clone 代碼運(yùn)行,一鍵完成 ButterKnife 的移除并替換成 ViewBinding 。
支持的語(yǔ)言與類
目前僅支持 Java
語(yǔ)言,因?yàn)橄嘈湃绻?xiàng)目中使用的是 Kotlin
,那肯定首選 KAE
或者 ViewBinding
了(優(yōu)選 ViewBinding ,如今 KAE 也已經(jīng)被移除了)。
該插件中目前對(duì)不同的類有不同的轉(zhuǎn)換方式
- Activity、Fragment、自定義 View 是移除 ButterKnife 并轉(zhuǎn)換成 ViewBinding
- ViewHolder、Dialog 是移除 ButterKnife 并轉(zhuǎn)換成 findViewById 形式
由于 Activity 與 Fragment 對(duì)于布局的塞入是比較統(tǒng)一的,所以可以做到比較精準(zhǔn)的轉(zhuǎn)換為 ViewBinding,而自定義 View 雖然布局的寫法也各式各樣,但是筆者也盡量修改統(tǒng)一了,而 ViewHolder 與 Dialog 比較復(fù)雜,直接修改成 findViewById 比較不容易出錯(cuò)(如果對(duì)自己的項(xiàng)目寫法的統(tǒng)一很有信心的,也可以按照自己項(xiàng)目的寫法試著修改一下代碼,都改成 ViewBinding 會(huì)更好),畢竟誰(shuí)也不希望修改后的代碼一團(tuán)糟是吧~
思路講解
研究代碼
首先我們需要研究一下使用了 ButterKnife 的代碼是怎么樣的,如果是自己使用過該插件的同學(xué)肯定是很了解、它的寫法的,而對(duì)于筆者這種沒使用過,但是公司的老項(xiàng)目中 java 的部分全是使用了 ButterKnife 的就很難受了,然后列出我們需要關(guān)心的注解。
- @BindView:用于標(biāo)記 xml 里的各種屬性
- @OnClick:用于標(biāo)記 xml 中屬性對(duì)應(yīng)的點(diǎn)擊事件
- @OnLongClick:用于標(biāo)記 xml 中屬性對(duì)應(yīng)的長(zhǎng)按事件
- @OnTouch:用于標(biāo)記 xml 中屬性對(duì)應(yīng)的 touch 事件
這里不做過多講解,畢竟又不是教大家怎么用 ButterKnife 是吧~
捋清思路
上面說到的相關(guān)注解是我們需要移除的,我們要針對(duì)我們轉(zhuǎn)換的不同方式對(duì)這些注解標(biāo)記的變量與方法做不同的操作。
- 對(duì)于修改成 findViewById 形式的類,我們只需要記錄下來該注解以及注解對(duì)應(yīng)的變量或者方法名稱,然后新增 initView() 方法用于初始化記錄下來的變量,新增 initListener() 方法用于點(diǎn)擊事件的編寫。
- 對(duì)于修改成 ViewBinding 形式的類,我們不僅需要記錄該注解與對(duì)應(yīng)的變量和方法,并且還需要遍歷類中的全部代碼,在檢索到該標(biāo)記的變量后,需要把這些變量都修改成 mBinding.xxx 的形式,注意:一般大家xml的id命名喜歡用_下劃線,但是ViewBinding使用的使用是需要自動(dòng)改成駝峰式命名的。
除此之外,我們需要移除的還有 ButterKnife 的 import 語(yǔ)句、綁定語(yǔ)句 bind()、以及解綁語(yǔ)句 unbind()。我們需要增加的有:layout 對(duì)應(yīng)的 ViewBinding 類的初始化語(yǔ)句、import 語(yǔ)句。
了解完這些我們就可以開始寫插件啦~
代碼編寫
對(duì)于代碼的編寫筆者這里也會(huì)分幾個(gè)步驟去闡述:分別是 PSI 相關(guān)知識(shí)、文件處理、編寫舉例、注意事項(xiàng)。
PSI相關(guān)知識(shí)
PSI 的全稱是 Program Structure Interface(程序結(jié)構(gòu)接口),我們要分析代碼以及修改代碼的話,是離不開 PSI 的,文檔傳送門
一個(gè) Class 文件結(jié)構(gòu)分別包含字段表、屬性表、方法表等,每個(gè)字段、方法也都有屬性表,但在 PSI
中,總體上只有 PsiFile
和 PsiElement
PsiFile
是一個(gè)接口,如果文件是一個(gè) java 文件,那么解析生成的 PsiFile 就是PsiJavaFile
對(duì)象,如果是一個(gè) Xml 文件,則解析后生成的是XmlFile
對(duì)象- 而對(duì)應(yīng) Java 文件的
PsiElement
種類有:PsiClass、PsiField、PsiMethod、PsiCodeBlock、PsiStatement、PsiMethodCallExpression 等等
其中,PsiJavaFile、PsiClass、PsiField、PsiMethod、PsiStatement 是我們本文涉及到的,大家可以先去看看文檔了解一下。
文件處理
我們?cè)谶x擇多級(jí)目錄的時(shí)候,會(huì)有很多的文件,而我們需要在這些文件中篩選出 java 文件,以及篩選出 import 語(yǔ)句中含有 butterknife 的,因?yàn)槿绻擃愂褂昧?ButterKnife ,則肯定需要 import 相關(guān)的類。
篩選 java 文件的這部分代碼在這里就不貼出來了,很簡(jiǎn)單的,大家可以直接去看代碼就好。
判斷該類是否需要進(jìn)行 ButterKnife 移除處理:
/** * 檢查是否有import butterknife相關(guān),若沒有引入butterknife,則不需要操作 */ private fun checkIsNeedModify(): Boolean { val importStatement = psiJavaFile.importList?.importStatements?.find { it.qualifiedName?.lowercase(Locale.getDefault())?.contains("butterknife") == true } return importStatement != null }
在這里需要先來一些前置知識(shí),我們的插件在獲取文件的的時(shí)候,拿到的是 VirtualFile
,當(dāng)該文件是java文件時(shí),VirtualFile 可以通過 PSI 提供的api轉(zhuǎn)換成 PsiJavaFile
,然后我們可以通過 PsiFile
拿到 PsiClass
,其中,importList 是屬于 PsiFile 的,而上面說到那些 PsiElement 都是屬于 PsiClass 的。
下面貼一下這部分代碼:
private fun handle(vFile: VirtualFile) { if (vFile.isDirectory) { handleDirectory(vFile) } else { // 判斷是否是java類型 if (vFile.fileType is JavaFileType) { // 轉(zhuǎn)換成psiFile val psiFile = PsiManager.getInstance(project!!).findFile(vFile) // 轉(zhuǎn)換成psiClass val psiClass = PsiTreeUtil.findChildOfAnyType(psiFile, PsiClass::class.java) handleSingleVirtualFile(vFile, psiFile, psiClass) } } }
這里只需要了解的就是添加了注釋的那幾行代碼。
編寫舉例
我們需要對(duì) PsiClass 進(jìn)行分類,這里目前是只能按照大部分人對(duì)類的命名習(xí)慣來進(jìn)行分析,如果有一些特殊的命名習(xí)慣的人,可以把代碼 clone 下來自行修改一下再運(yùn)行。
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")) { // 這里的判斷是為了不做處理,因?yàn)閍dapter的xml屬性是在viewHolder中初始化的 } else if (superType.contains("Dialog")) { DialogCodeParser(project, psiJavaFile, psiClass).execute() } else { // 自定義View CustomViewCodeParser(project, vFile, psiJavaFile, psiClass).execute() } }
我們通過拿到 PsiClass 繼承的父類的類型來進(jìn)行判斷,這里的不足是代碼中只拿了當(dāng)前類的上一級(jí)繼承的父類的類型,并沒有去判斷父類是否還有父類,因?yàn)楣P者認(rèn)為只要命名規(guī)范,這就不是什么大問題。舉個(gè)例子,如果有人喜歡封裝一個(gè)名為 BaseFragment 的實(shí)則是一個(gè) Activity 的基類,然后由 MainActivity 去繼承,那這個(gè)插件就不適用了??
這里要注意的是,我們此時(shí)只是判斷了外部類,而一個(gè) class 中可能會(huì)有多個(gè)內(nèi)部類,如 Adapter 中的 ViewHolder 就是一個(gè)很好的例子了,所以我們還需要遍歷每一個(gè) class 中的 innerClass,然后進(jìn)行同樣的操作:
// 內(nèi)部類處理 psiClass.innerClasses.forEach { checkClassType(it) }
由于涉及到的類別太多,所以這里只挑兩個(gè)例子出來解釋,分別是 ButterKnife 轉(zhuǎn)換為 ViewBinding 的 Activity、ButterKnife 轉(zhuǎn)換為 findViewById 的 ViewHolder,因?yàn)樯婕暗绞褂?PSI 分析并修改代碼,為了方便統(tǒng)一分析管理,所以這里抽了個(gè)基類。
下面先來看一下基類中兩個(gè)比較重要的方法,理解了這兩個(gè)方法后面的代碼才更容易理解: BaseCodeParser
private val bindViewFieldLists = mutableListOf<Pair<String, String>>() // 使用@BindView的屬性與單個(gè)字段 private val bindViewListFieldLists = mutableListOf<Triple<String, String, MutableList<String>>>() // 使用@BindView的屬性與多個(gè)字段 protected val innerBindViewFieldLists = mutableListOf<Pair<String, String>>() // 需要使用fvb形式的類 -- @BindView的屬性與單個(gè)字段 /** * 遍歷所有字段并找到@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個(gè)數(shù),若為多個(gè),則用另外的方式記錄處理 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 { // 否則直接記錄注解標(biāo)記的變量名稱與注解中的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標(biāo)記的方法 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}(" // 獲取該方法中的所有參數(shù),跟方法名一起拼接起來,方便后面直接調(diào)用 it.parameterList.parameters.forEachIndexed { index, params -> // 為了適配各種不同的命名,所以這里使用統(tǒng)一的命名 // 因?yàn)檫@三個(gè)注解只會(huì)存在這幾個(gè)類型的參數(shù) 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() } } } } } /** * 代碼寫入,修改的代碼統(tǒng)一使用該方法進(jìn)行修改寫入 */ private fun writeAction(commandName: String = "RemoveButterKnifeWriteAction", runnable: Runnable) { WriteCommandAction.runWriteCommandAction(project, commandName, "RemoveButterKnifeGroupID", runnable, psiJavaFile) }
這里的代碼可能會(huì)讓人有點(diǎn)懵,下面來解釋一下這些代碼,先解釋第一個(gè)方法:該方法是保存所有使用了 @BindView 注解標(biāo)記的變量,可以看到代碼中是分了 if else 去處理的,原因是有些代碼的 @BindView 中的 value 只有一個(gè),有些的會(huì)有多個(gè),多個(gè) value 的場(chǎng)景一般是使用 List 或者數(shù)組 Object[] 來進(jìn)行修飾的,如下例子:
如果注解中只有單個(gè) value,我們是可以直接改成 mBindind.xxx,而如果是 List 或者數(shù)組的形式的話,我們需要另外處理,這里筆者**使用的方式是記錄一個(gè)變量若對(duì)應(yīng)多個(gè) xml 屬性,則把這些屬性都添加進(jìn)該變量中,如 mTabViews.add(mBinding.xxx) **,要保證不影響原本的使用方式。
而第二個(gè)方法是保存所有使用了 @OnClick、@OnLongClick、@OnTouch 標(biāo)記的方法,同上,多個(gè)屬性的點(diǎn)擊事件可能會(huì)是同一個(gè)方法,如下例子:
看完了基類的兩個(gè)重要方法,下面我們來看一下對(duì)于我們的 Activity 要怎么轉(zhuǎn)換:
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 -> // 判斷布局在哪個(gè)statement中,并拿到R.layout.后面的名字 if (statement.text.trim().contains("R.layout.")) { val layoutRes = statement.text.trim().getLayoutRes() // 把布局名稱轉(zhuǎn)換成Binding實(shí)例名稱。如activity_record_detail -> ActivityRecordDetailBinding val bindingName = layoutRes.underLineToHump().withViewBinding() val afterStatement = elementFactory.createStatementFromText(statement.text.toString().replace("R.layout.$layoutRes", "mBinding.getRoot()"), psiClass) // 以下四個(gè)方法都在基類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標(biāo)記的變量改為mBinding.xxx changeBindViewStatement(statement) } } // 內(nèi)部類也可能使用外部類的變量 psiClass.innerClasses.forEach { it.methods.forEach { method -> method.body?.statements?.forEach { statement -> changeBindViewStatement(statement) } } } } override fun findClickInsertAnchor() { // 在onCreate中添加initListener方法,并把保存下來的監(jiān)聽事件寫入該方法中 val onCreateMethod = psiClass.findMethodsByName("onCreate", false)[0] insertOnClickMethod(onCreateMethod) } }
對(duì)于我們的 Activity,思路就是先找到 OnCreate() 方法,眾所周知,Activity 的 layout 布局是寫在 onCreate 中的 setContentView() 中的,所以我們需要找到這句 statement,拿到布局名稱,再轉(zhuǎn)換為駝峰式 + 首字母大寫,并在后面加上 Binding,這就是 ViewBinding 給我們布局生成的類名稱,不多做解釋,熟悉使用 ViewBinding 的人都會(huì)清楚的。
這里需要注意的是,上面的寫法只是常規(guī)的 layout 布局寫法,還有一些項(xiàng)目喜歡自行封裝的,比如喜歡把布局名稱寫在 getLayoutId() 中,然后在基類統(tǒng)一寫成 setContentView(getLayoutId())。使用這種寫法或者是其他封裝方式的童鞋可以自行修改一下代碼再運(yùn)行,因?yàn)榉庋b的方式太多了,這里無(wú)法做適配。
現(xiàn)在再來看一下上面未做解釋的幾個(gè)方法,首先來看一下 addBindingField()
,這是一個(gè)給class添加字段的方法:
val elementFactory = JavaPsiFacade.getInstance(project).elementFactory /** * 添加mBinding變量 */ protected fun addBindingField(fieldStr: String) { psiClass.addAfter(elementFactory.createFieldFromText(fieldStr, psiClass), psiClass.allFields.last()) }
elementFactory 是一個(gè) PsiElementFactory 對(duì)象,用于創(chuàng)建 PsiElement,也就是上面所介紹的各種 PsiElement 。這里我們需要先創(chuàng)建一個(gè) mBinding 變量,對(duì)于 Activity 我們可以直接通過 private bindingName mBinding = bindingName.inflate(getLayoutInflater());
去實(shí)例化 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 可能會(huì)引用很多個(gè) xml 屬性,而該注解標(biāo)記的字段可能是 List 也可能是數(shù)組,所以我們需要先判斷該字段是屬于哪種類型,并進(jìn)行初始化。這里需要注意的是:在遍歷添加字段的時(shí)候需要逆序添加,因?yàn)槲覀冊(cè)谔砑右痪?statement 的時(shí)候只有一個(gè)唯一參照物就是 new ArrayList<>() 或者是 new Objetc[] ,我們新添加的 statement 只能在這句代碼后面添加,所以實(shí)際上添加完后的代碼順序是倒過來的,需要逆序。
接下來看一下 changeBindingStatement()
方法:
/** * 修改mBinding的初始化語(yǔ)句 * @param method 需要修改的語(yǔ)句所在的方法 * @param beforeStatement 修改前的語(yǔ)句 * @param afterStatement 修改后的語(yǔ)句 */ protected fun changeBindingStatement(method: PsiMethod, beforeStatement: PsiStatement, afterStatement: PsiStatement) { writeAction { method.addAfter(afterStatement, beforeStatement) beforeStatement.delete() } }
這個(gè)方法沒什么好說的,結(jié)合上面的使用,就是把原本的 setContentView(R.layout.xxx)
改成 setContentView(mBinding.getRoot())
而已。
最后再來看一下 addImportStatement()
方法,這個(gè)方法是最復(fù)雜的,眾所周知,我們?cè)谑褂?ViewBinding 自動(dòng)生成的類時(shí)需要導(dǎo)包,但是這個(gè)包的路徑怎樣才能得到呢?由于我們一個(gè)項(xiàng)目中肯定會(huì)有多個(gè) module 以及多個(gè)目錄,我們無(wú)法確定當(dāng)前處理的文件所屬的是哪個(gè) module ,也無(wú)法確定當(dāng)前 module 中使用的 xml 文件是否是別的 module 的(畢竟 xml 文件是可以跨 module 使用的),由于不確定性太多導(dǎo)致無(wú)法正確拿到該 Binding 類的包名路徑進(jìn)行導(dǎo)包,所以我們需要采取別的措施。
我們都知道在開啟 ViewBinding 的開關(guān)的時(shí)候,我們每個(gè) xml 都會(huì)自動(dòng)生成對(duì)應(yīng)的 Binding 類,位于 build/generated/data_binding_base_class_source_out/debug/out
目錄中,這里我們只是帶過,我們真正需要的文件不在這里,我們真正需要拿的是每個(gè) Binding 類與所處的包名路徑的映射文件,位于 build/intermediates/data_binding_base_class_log_artifact/debug/out
中的一個(gè) json 文件,如下圖所示:
而這個(gè) json 文件只有在項(xiàng)目編譯過后才會(huì)生成,我們也可以通過執(zhí)行 task 去生成該文件,具體步驟后面會(huì)給出。
我們只需要解析這個(gè) json 文件,然后通過上面拿到的 Binding 名稱,再去拿對(duì)應(yīng)的 module_package
,就能拿到當(dāng)前的 Binding 類的路徑了,最后再通過 import 語(yǔ)句直接導(dǎo)包就好了。思路給了,由于代碼太長(zhǎng)篇幅有限,有興趣的可以直接去看代碼~
接下來我們來看一下如何把原本使用 @BindView 標(biāo)記的字段統(tǒng)一改成 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() } } } } }
當(dāng)我們匹配到我們記錄下來的字段以及對(duì)應(yīng)的 xml 屬性時(shí),我們就把匹配到的 statement 中含有該匹配值的地方替換成 mBinding.xxx ,這里需要注意的是:要考慮相似的單詞,如我們要匹配的是 view ,這時(shí)如果 statement 中含有 viewModel ,我們不能對(duì)它進(jìn)行處理,所以筆者這里用到了正則去判斷,對(duì)于項(xiàng)目中用到的一些方法都封裝在 StringExpand
中,有興趣的可以自行查看。
本來還想示例說明一下如何添加監(jiān)聽事件的,但是由于篇幅太長(zhǎng)了,這里就不貼代碼說明了,待會(huì)直接進(jìn)傳送門看吧~
好了,說完了 Activity 的處理,現(xiàn)在我們來看一下對(duì)于轉(zhuǎn)換為 findViewById 的 ViewHolder 我們?cè)趺刺幚戆蓗
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構(gòu)造函數(shù)的參數(shù)名稱 */ 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的綁定語(yǔ)句所在的方法 */ 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 的綁定語(yǔ)句所處的位置,一般是處于構(gòu)造函數(shù)中,然后我們需要拿到構(gòu)造函數(shù)中參數(shù)類型為 View 的參數(shù)名稱,因?yàn)橛行┤讼矚g命名為 view ,有些人喜歡命名為 itemView ,所以我們要拿到參數(shù)名稱后才可以添加 findViewById 語(yǔ)句,如 text = itemView.findViewById(R.id.text)
,這里還有一種別的情況就是構(gòu)造函數(shù)里可能沒有參數(shù)類型為 View 的參數(shù),這時(shí)我們只需要統(tǒng)一使用 itemView 就可以了。
ViewHolder 的轉(zhuǎn)換很簡(jiǎn)單,該解釋的方法上面也解釋了,沒解釋到的只能怪筆者太懶了??,懶得貼那么多代碼哈哈哈~
到這里我們已經(jīng)看完了 ButterKnife 分別轉(zhuǎn)換為 ViewBinding 、 findViewById 這兩種形式的代表類了,最后需要注意的是我們要修改并刪除完 ButterKnife 相關(guān)注解的時(shí)候,也要把相關(guān)的 ButterKnife.bind() 語(yǔ)句以及 import 語(yǔ)句刪掉
/** * 刪除ButterKnife的import語(yǔ)句、綁定語(yǔ)句、解綁語(yǔ)句 */ 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() } } }
注意事項(xiàng)
在前言說到的涉及到一些語(yǔ)法語(yǔ)義的聯(lián)系,代碼無(wú)法做到精準(zhǔn)轉(zhuǎn)換的時(shí)候說了后面會(huì)舉例說明,這里舉幾個(gè)常見的例子:
- 相關(guān)回調(diào)的參數(shù)名稱與 xml 中的屬性名稱一樣
@BindView(R.id.appBar) AppBarLayout appBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... appBar.addOnOffsetChangedListener((appBar, verticalOffset) -> { ... }); }
可以看到這里有兩個(gè) appBar ,一個(gè)是上面 @BindView 標(biāo)記的 appBar ,另一個(gè)是回調(diào)監(jiān)聽中的參數(shù),所以這里會(huì)不可避免的把兩個(gè) appBar 都修改成 mBinding.xxx ,但是在修改回調(diào)參數(shù)的 appBar 時(shí),這個(gè)類會(huì)報(bào)錯(cuò),所以后面在查看出錯(cuò)的類時(shí)會(huì)看到這個(gè)錯(cuò)誤。這種情況可以通過修改回調(diào)參數(shù)的名稱解決,修改之后再重新執(zhí)行一次就可以了。
- @BindView 標(biāo)記的字段是 layout 中某個(gè)自定義 View 里的 xml 屬性
這個(gè)就不貼代碼舉例子了,總的來說就是假設(shè) MainActivity 中的布局是 activity_main ,該布局中含有一個(gè) CustomView ,而 CustomView 中有一個(gè)布局 layout_custom_view ,而 layout_custom_view 中有一個(gè) TextView 的 id 是 tv_content ,而這個(gè) tv_content 是可以通過 ButterKnife 直接在 MainActivity 中使用的,但是修改成 ViewBinding 之后是拿不到這個(gè) mBinding.tvContent 的(不知道我這么說大家能不能理解??)
- Activity 中通過 if else 判斷 setContentView 需要塞入哪個(gè)布局
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... if (xxx > 0) { setContentView(R.layout.layout1); } else { setContentView(R.layout.layout2); } }
這種情況真的是不知道該實(shí)例化哪個(gè) Binding 類,還是老老實(shí)實(shí)的手動(dòng)修改成 findViewById 吧。
使用步驟
- 在項(xiàng)目中開啟 ViewBinding
android { viewBinding { enabled = true } }
- 生成 ViewBinding 相關(guān)的類
在項(xiàng)目目錄下執(zhí)行 ./gradlew dataBindingGenBaseClassesDebug
生成 ViewBinding 相關(guān)的類與映射文件
- 執(zhí)行代碼轉(zhuǎn)換
右鍵需要轉(zhuǎn)換的文件目錄(支持單個(gè)文件操作或多級(jí)目錄操作),點(diǎn)擊 RemoveButterKnife 開始轉(zhuǎn)換,如果文件很多的話需要等待的時(shí)候會(huì)久一點(diǎn)。
- 等待執(zhí)行結(jié)果
結(jié)果如下所示,有異常的文件可以手動(dòng)檢查并自行解決。
注意:轉(zhuǎn)換完之后一定一定一定要檢查一遍,最好打包讓測(cè)試也重新測(cè)一遍?。?!
以上就是一鍵移除ButterKnife并替換為ViewBinding的舊項(xiàng)目拯救的詳細(xì)內(nèi)容,更多關(guān)于ButterKnife替換ViewBinding的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)webview實(shí)例代碼
本篇文章主要介紹了Android實(shí)現(xiàn)webview實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Android中外接鍵盤的檢測(cè)的實(shí)現(xiàn)
這篇文章主要介紹了Android中外接鍵盤的檢測(cè)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Flutter 用自定義轉(zhuǎn)場(chǎng)動(dòng)畫實(shí)現(xiàn)頁(yè)面切換
本篇介紹了 fluro 導(dǎo)航到其他頁(yè)面的自定義轉(zhuǎn)場(chǎng)動(dòng)畫實(shí)現(xiàn),F(xiàn)lutter本身提供了不少預(yù)定義的轉(zhuǎn)場(chǎng)動(dòng)畫,可以通過 transitionBuilder 參數(shù)設(shè)計(jì)多種多樣的轉(zhuǎn)場(chǎng)動(dòng)畫,也可以通過自定義的 AnimatedWidget實(shí)現(xiàn)個(gè)性化的轉(zhuǎn)場(chǎng)動(dòng)畫效果。2021-06-06Android開發(fā)實(shí)現(xiàn)ListView點(diǎn)擊item改變顏色功能示例
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)ListView點(diǎn)擊item改變顏色功能,涉及Android布局及響應(yīng)事件動(dòng)態(tài)變換元素屬性相關(guān)操作技巧,需要的朋友可以參考下2017-11-11Android自定義View實(shí)現(xiàn)分段選擇按鈕的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android自定義View實(shí)現(xiàn)分段選擇按鈕的實(shí)現(xiàn)代碼,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12Android 優(yōu)化之卡頓優(yōu)化的實(shí)現(xiàn)
這篇文章主要介紹了Android 優(yōu)化之卡頓優(yōu)化的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07