一鍵移除ButterKnife并替換為ViewBinding的舊項目拯救
前言
眾所周知,黃油刀 ButterKnife 已經(jīng)廢棄了,并且已經(jīng)不再維護了,而一些老項目估計還有一堆這樣的代碼,相信大家多多少少都有過被 @BindView
或者 @OnClick
支配的恐懼,而如果想要一個頁面一個頁面的移除的話,工作量也是非常大的,而這也是筆者寫這個插件的原因了(這里不講解插件開發(fā)的相關(guān)知識)。
注:由于每個項目的封裝的多樣性、以及 layout 布局的初始化有各種各樣的寫法,還有涉及到一些語法語義的聯(lián)系,代碼無法做到精準(zhǔn)轉(zhuǎn)換(后面會舉一些例子),所以插件無法做到百分百轉(zhuǎn)換成功,在轉(zhuǎn)換后建議手動檢查一下是否出錯。
本文對于沒有插件開發(fā)以及 PSI 基礎(chǔ)的人可能會看不下去,可以直接 github傳送門 跳 github 鏈接并 clone 代碼運行,一鍵完成 ButterKnife 的移除并替換成 ViewBinding 。
支持的語言與類
目前僅支持 Java
語言,因為相信如果項目中使用的是 Kotlin
,那肯定首選 KAE
或者 ViewBinding
了(優(yōu)選 ViewBinding ,如今 KAE 也已經(jīng)被移除了)。
該插件中目前對不同的類有不同的轉(zhuǎn)換方式
- Activity、Fragment、自定義 View 是移除 ButterKnife 并轉(zhuǎn)換成 ViewBinding
- ViewHolder、Dialog 是移除 ButterKnife 并轉(zhuǎn)換成 findViewById 形式
由于 Activity 與 Fragment 對于布局的塞入是比較統(tǒng)一的,所以可以做到比較精準(zhǔn)的轉(zhuǎn)換為 ViewBinding,而自定義 View 雖然布局的寫法也各式各樣,但是筆者也盡量修改統(tǒng)一了,而 ViewHolder 與 Dialog 比較復(fù)雜,直接修改成 findViewById 比較不容易出錯(如果對自己的項目寫法的統(tǒng)一很有信心的,也可以按照自己項目的寫法試著修改一下代碼,都改成 ViewBinding 會更好),畢竟誰也不希望修改后的代碼一團糟是吧~
思路講解
研究代碼
首先我們需要研究一下使用了 ButterKnife 的代碼是怎么樣的,如果是自己使用過該插件的同學(xué)肯定是很了解、它的寫法的,而對于筆者這種沒使用過,但是公司的老項目中 java 的部分全是使用了 ButterKnife 的就很難受了,然后列出我們需要關(guān)心的注解。
- @BindView:用于標(biāo)記 xml 里的各種屬性
- @OnClick:用于標(biāo)記 xml 中屬性對應(yīng)的點擊事件
- @OnLongClick:用于標(biāo)記 xml 中屬性對應(yīng)的長按事件
- @OnTouch:用于標(biāo)記 xml 中屬性對應(yīng)的 touch 事件
這里不做過多講解,畢竟又不是教大家怎么用 ButterKnife 是吧~
捋清思路
上面說到的相關(guān)注解是我們需要移除的,我們要針對我們轉(zhuǎn)換的不同方式對這些注解標(biāo)記的變量與方法做不同的操作。
- 對于修改成 findViewById 形式的類,我們只需要記錄下來該注解以及注解對應(yīng)的變量或者方法名稱,然后新增 initView() 方法用于初始化記錄下來的變量,新增 initListener() 方法用于點擊事件的編寫。
- 對于修改成 ViewBinding 形式的類,我們不僅需要記錄該注解與對應(yīng)的變量和方法,并且還需要遍歷類中的全部代碼,在檢索到該標(biāo)記的變量后,需要把這些變量都修改成 mBinding.xxx 的形式,注意:一般大家xml的id命名喜歡用_下劃線,但是ViewBinding使用的使用是需要自動改成駝峰式命名的。
除此之外,我們需要移除的還有 ButterKnife 的 import 語句、綁定語句 bind()、以及解綁語句 unbind()。我們需要增加的有:layout 對應(yīng)的 ViewBinding 類的初始化語句、import 語句。
了解完這些我們就可以開始寫插件啦~
代碼編寫
對于代碼的編寫筆者這里也會分幾個步驟去闡述:分別是 PSI 相關(guān)知識、文件處理、編寫舉例、注意事項。
PSI相關(guān)知識
PSI 的全稱是 Program Structure Interface(程序結(jié)構(gòu)接口),我們要分析代碼以及修改代碼的話,是離不開 PSI 的,文檔傳送門
一個 Class 文件結(jié)構(gòu)分別包含字段表、屬性表、方法表等,每個字段、方法也都有屬性表,但在 PSI
中,總體上只有 PsiFile
和 PsiElement
PsiFile
是一個接口,如果文件是一個 java 文件,那么解析生成的 PsiFile 就是PsiJavaFile
對象,如果是一個 Xml 文件,則解析后生成的是XmlFile
對象- 而對應(yīng) Java 文件的
PsiElement
種類有:PsiClass、PsiField、PsiMethod、PsiCodeBlock、PsiStatement、PsiMethodCallExpression 等等
其中,PsiJavaFile、PsiClass、PsiField、PsiMethod、PsiStatement 是我們本文涉及到的,大家可以先去看看文檔了解一下。
文件處理
我們在選擇多級目錄的時候,會有很多的文件,而我們需要在這些文件中篩選出 java 文件,以及篩選出 import 語句中含有 butterknife 的,因為如果該類使用了 ButterKnife ,則肯定需要 import 相關(guān)的類。
篩選 java 文件的這部分代碼在這里就不貼出來了,很簡單的,大家可以直接去看代碼就好。
判斷該類是否需要進行 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 }
在這里需要先來一些前置知識,我們的插件在獲取文件的的時候,拿到的是 VirtualFile
,當(dāng)該文件是java文件時,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) } } }
這里只需要了解的就是添加了注釋的那幾行代碼。
編寫舉例
我們需要對 PsiClass 進行分類,這里目前是只能按照大部分人對類的命名習(xí)慣來進行分析,如果有一些特殊的命名習(xí)慣的人,可以把代碼 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 繼承的父類的類型來進行判斷,這里的不足是代碼中只拿了當(dāng)前類的上一級繼承的父類的類型,并沒有去判斷父類是否還有父類,因為筆者認為只要命名規(guī)范,這就不是什么大問題。舉個例子,如果有人喜歡封裝一個名為 BaseFragment 的實則是一個 Activity 的基類,然后由 MainActivity 去繼承,那這個插件就不適用了??
這里要注意的是,我們此時只是判斷了外部類,而一個 class 中可能會有多個內(nèi)部類,如 Adapter 中的 ViewHolder 就是一個很好的例子了,所以我們還需要遍歷每一個 class 中的 innerClass,然后進行同樣的操作:
// 內(nèi)部類處理 psiClass.innerClasses.forEach { checkClassType(it) }
由于涉及到的類別太多,所以這里只挑兩個例子出來解釋,分別是 ButterKnife 轉(zhuǎn)換為 ViewBinding 的 Activity、ButterKnife 轉(zhuǎn)換為 findViewById 的 ViewHolder,因為涉及到使用 PSI 分析并修改代碼,為了方便統(tǒng)一分析管理,所以這里抽了個基類。
下面先來看一下基類中兩個比較重要的方法,理解了這兩個方法后面的代碼才更容易理解: 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個數(shù),若為多個,則用另外的方式記錄處理 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)一的命名 // 因為這三個注解只會存在這幾個類型的參數(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)一使用該方法進行修改寫入 */ private fun writeAction(commandName: String = "RemoveButterKnifeWriteAction", runnable: Runnable) { WriteCommandAction.runWriteCommandAction(project, commandName, "RemoveButterKnifeGroupID", runnable, psiJavaFile) }
這里的代碼可能會讓人有點懵,下面來解釋一下這些代碼,先解釋第一個方法:該方法是保存所有使用了 @BindView 注解標(biāo)記的變量,可以看到代碼中是分了 if else 去處理的,原因是有些代碼的 @BindView 中的 value 只有一個,有些的會有多個,多個 value 的場景一般是使用 List 或者數(shù)組 Object[] 來進行修飾的,如下例子:
如果注解中只有單個 value,我們是可以直接改成 mBindind.xxx,而如果是 List 或者數(shù)組的形式的話,我們需要另外處理,這里筆者**使用的方式是記錄一個變量若對應(yīng)多個 xml 屬性,則把這些屬性都添加進該變量中,如 mTabViews.add(mBinding.xxx) **,要保證不影響原本的使用方式。
而第二個方法是保存所有使用了 @OnClick、@OnLongClick、@OnTouch 標(biāo)記的方法,同上,多個屬性的點擊事件可能會是同一個方法,如下例子:
看完了基類的兩個重要方法,下面我們來看一下對于我們的 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 -> // 判斷布局在哪個statement中,并拿到R.layout.后面的名字 if (statement.text.trim().contains("R.layout.")) { val layoutRes = statement.text.trim().getLayoutRes() // 把布局名稱轉(zhuǎn)換成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標(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) } }
對于我們的 Activity,思路就是先找到 OnCreate() 方法,眾所周知,Activity 的 layout 布局是寫在 onCreate 中的 setContentView() 中的,所以我們需要找到這句 statement,拿到布局名稱,再轉(zhuǎn)換為駝峰式 + 首字母大寫,并在后面加上 Binding,這就是 ViewBinding 給我們布局生成的類名稱,不多做解釋,熟悉使用 ViewBinding 的人都會清楚的。
這里需要注意的是,上面的寫法只是常規(guī)的 layout 布局寫法,還有一些項目喜歡自行封裝的,比如喜歡把布局名稱寫在 getLayoutId() 中,然后在基類統(tǒng)一寫成 setContentView(getLayoutId())。使用這種寫法或者是其他封裝方式的童鞋可以自行修改一下代碼再運行,因為封裝的方式太多了,這里無法做適配。
現(xiàn)在再來看一下上面未做解釋的幾個方法,首先來看一下 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 對象,用于創(chuàng)建 PsiElement,也就是上面所介紹的各種 PsiElement 。這里我們需要先創(chuàng)建一個 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 屬性,而該注解標(biāo)記的字段可能是 List 也可能是數(shù)組,所以我們需要先判斷該字段是屬于哪種類型,并進行初始化。這里需要注意的是:在遍歷添加字段的時候需要逆序添加,因為我們在添加一句 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() } }
這個方法沒什么好說的,結(jié)合上面的使用,就是把原本的 setContentView(R.layout.xxx)
改成 setContentView(mBinding.getRoot())
而已。
最后再來看一下 addImportStatement()
方法,這個方法是最復(fù)雜的,眾所周知,我們在使用 ViewBinding 自動生成的類時需要導(dǎo)包,但是這個包的路徑怎樣才能得到呢?由于我們一個項目中肯定會有多個 module 以及多個目錄,我們無法確定當(dāng)前處理的文件所屬的是哪個 module ,也無法確定當(dāng)前 module 中使用的 xml 文件是否是別的 module 的(畢竟 xml 文件是可以跨 module 使用的),由于不確定性太多導(dǎo)致無法正確拿到該 Binding 類的包名路徑進行導(dǎo)包,所以我們需要采取別的措施。
我們都知道在開啟 ViewBinding 的開關(guān)的時候,我們每個 xml 都會自動生成對應(yīng)的 Binding 類,位于 build/generated/data_binding_base_class_source_out/debug/out
目錄中,這里我們只是帶過,我們真正需要的文件不在這里,我們真正需要拿的是每個 Binding 類與所處的包名路徑的映射文件,位于 build/intermediates/data_binding_base_class_log_artifact/debug/out
中的一個 json 文件,如下圖所示:
而這個 json 文件只有在項目編譯過后才會生成,我們也可以通過執(zhí)行 task 去生成該文件,具體步驟后面會給出。
我們只需要解析這個 json 文件,然后通過上面拿到的 Binding 名稱,再去拿對應(yīng)的 module_package
,就能拿到當(dāng)前的 Binding 類的路徑了,最后再通過 import 語句直接導(dǎo)包就好了。思路給了,由于代碼太長篇幅有限,有興趣的可以直接去看代碼~
接下來我們來看一下如何把原本使用 @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)我們匹配到我們記錄下來的字段以及對應(yīng)的 xml 屬性時,我們就把匹配到的 statement 中含有該匹配值的地方替換成 mBinding.xxx ,這里需要注意的是:要考慮相似的單詞,如我們要匹配的是 view ,這時如果 statement 中含有 viewModel ,我們不能對它進行處理,所以筆者這里用到了正則去判斷,對于項目中用到的一些方法都封裝在 StringExpand
中,有興趣的可以自行查看。
本來還想示例說明一下如何添加監(jiān)聽事件的,但是由于篇幅太長了,這里就不貼代碼說明了,待會直接進傳送門看吧~
好了,說完了 Activity 的處理,現(xiàn)在我們來看一下對于轉(zhuǎn)換為 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構(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的綁定語句所在的方法 */ 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 的綁定語句所處的位置,一般是處于構(gòu)造函數(shù)中,然后我們需要拿到構(gòu)造函數(shù)中參數(shù)類型為 View 的參數(shù)名稱,因為有些人喜歡命名為 view ,有些人喜歡命名為 itemView ,所以我們要拿到參數(shù)名稱后才可以添加 findViewById 語句,如 text = itemView.findViewById(R.id.text)
,這里還有一種別的情況就是構(gòu)造函數(shù)里可能沒有參數(shù)類型為 View 的參數(shù),這時我們只需要統(tǒng)一使用 itemView 就可以了。
ViewHolder 的轉(zhuǎn)換很簡單,該解釋的方法上面也解釋了,沒解釋到的只能怪筆者太懶了??,懶得貼那么多代碼哈哈哈~
到這里我們已經(jīng)看完了 ButterKnife 分別轉(zhuǎn)換為 ViewBinding 、 findViewById 這兩種形式的代表類了,最后需要注意的是我們要修改并刪除完 ButterKnife 相關(guān)注解的時候,也要把相關(guān)的 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() } } }
注意事項
在前言說到的涉及到一些語法語義的聯(lián)系,代碼無法做到精準(zhǔn)轉(zhuǎn)換的時候說了后面會舉例說明,這里舉幾個常見的例子:
- 相關(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) -> { ... }); }
可以看到這里有兩個 appBar ,一個是上面 @BindView 標(biāo)記的 appBar ,另一個是回調(diào)監(jiān)聽中的參數(shù),所以這里會不可避免的把兩個 appBar 都修改成 mBinding.xxx ,但是在修改回調(diào)參數(shù)的 appBar 時,這個類會報錯,所以后面在查看出錯的類時會看到這個錯誤。這種情況可以通過修改回調(diào)參數(shù)的名稱解決,修改之后再重新執(zhí)行一次就可以了。
- @BindView 標(biāo)記的字段是 layout 中某個自定義 View 里的 xml 屬性
這個就不貼代碼舉例子了,總的來說就是假設(shè) 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 相關(guān)的類
在項目目錄下執(zhí)行 ./gradlew dataBindingGenBaseClassesDebug
生成 ViewBinding 相關(guān)的類與映射文件
- 執(zhí)行代碼轉(zhuǎn)換
右鍵需要轉(zhuǎn)換的文件目錄(支持單個文件操作或多級目錄操作),點擊 RemoveButterKnife 開始轉(zhuǎn)換,如果文件很多的話需要等待的時候會久一點。
- 等待執(zhí)行結(jié)果
結(jié)果如下所示,有異常的文件可以手動檢查并自行解決。
注意:轉(zhuǎn)換完之后一定一定一定要檢查一遍,最好打包讓測試也重新測一遍?。。?/p>
以上就是一鍵移除ButterKnife并替換為ViewBinding的舊項目拯救的詳細內(nèi)容,更多關(guān)于ButterKnife替換ViewBinding的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Flutter 用自定義轉(zhuǎn)場動畫實現(xiàn)頁面切換
本篇介紹了 fluro 導(dǎo)航到其他頁面的自定義轉(zhuǎn)場動畫實現(xiàn),F(xiàn)lutter本身提供了不少預(yù)定義的轉(zhuǎn)場動畫,可以通過 transitionBuilder 參數(shù)設(shè)計多種多樣的轉(zhuǎn)場動畫,也可以通過自定義的 AnimatedWidget實現(xiàn)個性化的轉(zhuǎn)場動畫效果。2021-06-06Android開發(fā)實現(xiàn)ListView點擊item改變顏色功能示例
這篇文章主要介紹了Android開發(fā)實現(xiàn)ListView點擊item改變顏色功能,涉及Android布局及響應(yīng)事件動態(tài)變換元素屬性相關(guān)操作技巧,需要的朋友可以參考下2017-11-11Android自定義View實現(xiàn)分段選擇按鈕的實現(xiàn)代碼
這篇文章主要介紹了Android自定義View實現(xiàn)分段選擇按鈕的實現(xiàn)代碼,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12Android 優(yōu)化之卡頓優(yōu)化的實現(xiàn)
這篇文章主要介紹了Android 優(yōu)化之卡頓優(yōu)化的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07