A plug-in for the Synthetic migration ViewBinding

Recently, the company planned to transfer the original Synthetic to ViewBinding. It would be painful to change the underline to hump, so I simply wrote a plug-in.

Why migrate

For more on why to migrate, check out this article Kotlin updated version 1.5: Synthetic Crime Analysis

To sum up:

  • Efficiency issues caused by changes in view cache mechanism after Kotlin upgrade 1.5;
  • Wide-eyed taking a view using Kotlin Synwide-eyed might cause a null pointer;
  • Synthetic has expired, according to the inertia from disappear not long (manual dog head);

Implementation using delegate properties

We ultimately chose the implementation of KotlinDelegate internally. For analysis and principles, see the original author’s article:

Android | ViewBinding and Kotlin entrust shuangjian combination

Based on the above wheels, there are two main pieces of code that we will eventually need to migrate:

  • Implementation of viewBinding:

      private val binding by viewBinding(LayoutTestBinding::bind)
    Copy the code
  • Change the name of the underline in the original code to the name of the camel:

    app_detail -> binding.appDetail

The first implementation of viewBinding is relatively simple, and the most tedious is the modification of the hump. Therefore, the main purpose of this plug-in is to quickly transition the existing code.

The main implementation

From the editor checked r.layout. name, find the corresponding XML and parse all ids in it:

   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)
        returnlayout ? : findLayoutResource(candidateB) }private fun findLayoutResource(element: PsiElement?).: PsiFile? {
        if (element == null) {
            return null // nothing to be used
        }
        vallayout: 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]}Copy the code

Based on the ID set found, replace the original ID with hump by text:

fun doWriteAction(viewIdInfoAsList: List<ViewInfo>) {
    writeCommandAction(mProject).run<Exception> {
        try {
            var classStr = mClass.text
            // Replace in descending order of text length to prevent repeated replacement of child elements
            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)) asKtClass mClass.body? .addAfter(mFactory.createProperty(bindingCode), mClass.body? .firstChild) }catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
Copy the code

Plug-in installation

  • Take the bag down github.com/MartinHY/Sy…
  • Idea Local Installation
  • Delete 0.0 after migration

Plug-in usage

Select the Layout ID in the code and right-click Synthetic2ViewBinding:

Generate the relevant replacement code:

A few problems with plug-ins

  • ViewBinding is not supported for internal classes. The viewBinding will only be generated at the bottom of the construct.

  • Because the plugin is replaced by ktClass text globally, it does not check whether it is a method or a variable. It is recommended to check the comparison line by line (RollBackChange is very useful).

  • Plug-ins are only responsible for solving the efficiency problem of migration, and relevant functions need to be verified by themselves.