用于 Synthetic 迁移 ViewBinding 的插件
最近公司项目要将原有的 Synthetic 迁移到 ViewBinding , 想着要把下划线改成驼峰就蛋疼 , 索性动手写了一个插件 ;
为什么要进行迁移
关于为什么要迁移 , 可以去看看这篇文章 Kotlin升级1.5版本synthetic引发的血案分析
总结来说 :
- kotlin 升级 1.5 之后 view cache 缓存机制的变化导致的效率问题 ;
- 使用 Kotlin Synthetics 获取 view 可能会导致 null pointer ;
- Synthetic 已经过期 , 按照惯性离消失也没多久了(手动狗头) ;
使用委托属性的方式进行实现
我们内部最终选用的是 KotlinDelegate 的实现 , 相关分析和原理可以看看原作者的文章 :
Android | ViewBinding 与 Kotlin 委托双剑合璧
根据上面的轮子 , 我们最终需要迁移的代码主要有两个 :
-
viewbinding 的实现 :
private val binding by viewBinding(LayoutTestBinding::bind)
-
将原有代码中的下划线命名修改为驼峰的命名方式 :
app_detail -> binding.appDetail
第一个 viewbinding 的实现 , 其实相对比较简单 , 最繁琐的莫过于驼峰的修改 , 所以这个插件的主要目的也是想在已有代码中快速过渡 ;
主要实现
从 editor 选中的 R.layout.name 中 , 找到对应的 xml 并解析其中的所有 id :
fun getLayoutFileFromCaret(file: PsiFile, editor: Editor): PsiFile? {
val offset = editor.caretModel.offset
val candidateA = file.findElementAt(offset)
val candidateB = file.findElementAt(offset - 1)
val layout = findLayoutResource(candidateA)
return layout ?: findLayoutResource(candidateB)
}
private fun findLayoutResource(element: PsiElement?): PsiFile? {
if (element == null) {
return null // nothing to be used
}
val layout: PsiElement = element.parent.parent.firstChild
?: return null
if ("R.layout" != layout.text) {
return null // not layout file
}
val name = String.format("%s.xml", element.text)
return resolveLayoutResourceFile(element, name)
}
fun resolveLayoutResourceFile(element: PsiElement?, layoutName: String?): PsiFile? {
if (element == null || layoutName == null) return null
val project = element.project
val module = ModuleUtil.findModuleForPsiElement(element)
var files: Array<PsiFile>? = null
if (module != null) {
val moduleScope = module.getModuleWithDependenciesAndLibrariesScope(false)
files = FilenameIndex.getFilesByName(project, layoutName, moduleScope)
}
if (files == null || files.isEmpty()) {
files = FilenameIndex.getFilesByName(project, layoutName, everythingScope(project))
}
return if (files.isEmpty()) {
null //no matching files
} else files[0]
}
根据找到的 ID 集合 , 通过文本替换原有的 id 为驼峰 :
fun doWriteAction(viewIdInfoAsList: List<ViewInfo>) {
writeCommandAction(mProject).run<Exception> {
try {
var classStr = mClass.text
//按文字长度降序进行替换 , 以防重复替换子元素
val viewList = viewIdInfoAsList.sortedByDescending {
it.id.length
}
for (info in viewList) {
var str = String.format(
Constants.COMMON_VIEW,
info.id
)
if (info.id.contains("_")){
str = String.format(
Constants.COMMON_VIEW,
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, info.id)
)
}
println("SynthiticReplaceMethod info.id ${info.id} str $str")
classStr = classStr.replace(
info.id, str
)
}
mClass = mClass.replace(mFactory.createClass(classStr)) as KtClass
mClass.body?.addAfter(mFactory.createProperty(bindingCode), mClass.body?.firstChild)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
插件安装
- 把包下下来 https://github.com/kongnanlive/Synthetic2ViewBinding/releases/tag/v1.0.2
- idea 本地安装
- 迁移完删除 0.0
插件使用方式
选中代码中的 layout id , 右键选中 Synthetic2ViewBinding :
生成相关替换代码 :
插件的几个问题
-
不支持内部类的相关实现 , viewBinding 只会生成在构造的下面 , 请自行拷贝 , 驼峰的替换是全局的 ;
-
关于命名重复的问题 , 由于是插件是通过 ktClass 的文本全局替换的 , 所以不会检测是否是方法还是变量啥的 , 统一经过文本替换, 建议自行逐行检验对比( RollBackChange 就很好用) ;
-
插件只负责解决迁移的效率问题 , 相关功能还是需要自行进行校验的 ;