Android: Autmomate Migrate from Kotlin synthetics to Jetpack view binding

1.2k Views Asked by At

As the 'kotlin-android-extensions' is deprecated, and I have some library updates pending that depend on this migration.

I have seen the migration guide, which suggests to manually do some changes for migration.

Now my Android application has 100+ uses of 'kotlinx.android.synthetic'. Is there any tool or script that can automatically do the migration? Or are they nuts?

2

There are 2 best solutions below

1
ΓDΛ On BEST ANSWER

Unfortunately, there is no script and tool that does it automatically. But to make the process easier, I can suggest a practical use. The article below will help.

Fast migration from Kotlin Synthetics to View Binding

2
Himanshu On

I have created a script in kotlin (scratch file). This script might help you to reduce your manual work by 60-70%, also it have some conditions which is more specific to my code base, so suggesting you to make some small changes in script as well to match with your codebase.

Script details:
Script written in : kotlin
Code coverage: Fragment/DialogFragment/BottomSheet/Activity/ViewHolders

Replaceable code :
inputFileName : list of all kt files
dir : "directory path of src folder with all kt files"
layoutDir : "layout directory path"
layoutV21Dir : "layout-21 directory path"
function definations of isCustomView, isViewHolder

import java.io.FileNotFoundException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.util.Locale
import kotlin.io.path.pathString

val inputFileName = mutableListOf("Provide your .kt files")
val dir = "/home/Office/git/android/my_project/src/main/java/com/xyz/app/"
val snakeRegex = "_[a-zA-Z]".toRegex()
val layoutDir = "/home/Office/git/android/my_project/src/main/java/com/xyz/res/layout/"
val layoutV21Dir = "/home/Office/git/android/my_project/src/main/java/com/xyz/app/res/layout-v21/"
val defineBindingPlaceholder = "<binding_placeholder>"
val importBindingPlaceholder = "<import_binding_placeholder>"
var layoutDetailList : MutableList<LayoutDetails> = mutableListOf()

data class LayoutDetails(
    var ktFileName: String? = null,
    var defineBinding: Boolean = false,
    var importBinding: Boolean = false,
    var bindingName:String? = null,
    var layoutName:String? = null,
    val ids:MutableList<String> = mutableListOf(),
    val includeLayoutDetail:MutableList<IncludeLayoutDetails> = mutableListOf(),
    val viewStubLayoutDetail:MutableList<IncludeLayoutDetails> = mutableListOf()
)

data class IncludeLayoutDetails(
    var layoutName:String? = null, var layoutPath:String? = null,
    var layoutId:String? = null,
    var bindingName:String? = null, val ids:MutableList<String> = mutableListOf())

fun prepareEligibleFiles(): MutableMap<String, String> {
    val allEligibleFiles = mutableMapOf<String, String>()
    Files.walkFileTree(Paths.get(dir), object : SimpleFileVisitor<Path>() {
    override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
        val fileName = file.fileName.toString().trim()
        if(!fileName.contains("Adapter")
            && inputFileName.contains(fileName) && attrs.isRegularFile){
            allEligibleFiles[fileName] = file.pathString
        }
        return FileVisitResult.CONTINUE
    }
    })
    return allEligibleFiles
}

start()

fun start(){
    val allEligibleFiles = prepareEligibleFiles()
    allEligibleFiles.forEach {
    val layoutDetails = LayoutDetails()
    layoutDetails.ktFileName = it.key
    try{
        val addBindingPlaceHolder = addBindingPlaceHolder(layoutDetails, it.value, it.key)
        val layoutName = layoutDetails.layoutName
        if(!layoutName.isNullOrEmpty()){
            resolveAllIds(layoutName, layoutDetails)
        }
        println(layoutDetails)
        startWritingBindings(addBindingPlaceHolder, it.value, layoutDetails)
        startReplacingIds(layoutDetails, it.value)
        layoutDetailList.add(layoutDetails)
    }catch (e:FileNotFoundException){
        println("File ${it.key} skipped due to ${e.message}")
    }
    }
}

fun startWritingBindings(addBindingPlaceHolder: List<String>, filePath: String, layoutDetails: LayoutDetails) {
    val fragRegex = Regex("onCreateView\\s*\\(")
    Files.write(Path.of(filePath),
    addBindingPlaceHolder.map {line ->
        if(line.contains(defineBindingPlaceholder)) {
            if(layoutDetails.defineBinding && !layoutDetails.bindingName.isNullOrEmpty()){
                if(addBindingPlaceHolder.any { fragRegex.containsMatchIn(it) }){
                    line.replace(defineBindingPlaceholder, "private var _mBinding: ${layoutDetails.bindingName}? = null\n\tprivate val mBinding get() = _mBinding!!")
                }else{
                    line.replace(defineBindingPlaceholder, "private lateinit var mBinding: ${layoutDetails.bindingName}")
                }
            }else{
                println("Define binding was there for file ${layoutDetails.ktFileName} but some issue in $layoutDetails")
                line
            }
        }else if(line.contains(importBindingPlaceholder)){
            if(!layoutDetails.bindingName.isNullOrEmpty() && layoutDetails.importBinding){
                line.replace(importBindingPlaceholder, layoutDetails.bindingName!!)
            }else{
                println("Import binding was there for file ${layoutDetails.ktFileName} but some issue in $layoutDetails")
                line
            }
        }else{
            line
        }
    }
    )
}

fun startReplacingIds(layoutDetails: LayoutDetails, filePath: String) {
    val path = Paths.get(filePath)
    var content = String(Files.readAllBytes(path))
    val charset: Charset = StandardCharsets.UTF_8
    println("\n**startReplacingIds")
    println("${layoutDetails.layoutName} ids ${layoutDetails.ids} include/viewstub ${layoutDetails.includeLayoutDetail}")
    layoutDetails.includeLayoutDetail.forEach {include->
    println("Include layout started $include all ids ${include.ids}")
    include.ids.forEach {
        val snakeToLowerCamelCase = it.snakeToLowerCamelCase()
        val findViewGenericPattern = "((${include.layoutId}\\.)|(${include.layoutId}\\?.))findViewById<[^>]+>\\(R\\.id\\.$it\\)(\\.|\\?.|$)".toRegex()
        val findViewBlankGenericPattern = "((${include.layoutId}\\.)|(${include.layoutId}\\?.))findViewById<>\\(R\\.id\\.$it\\)(\\.|\\?.|\$)".toRegex()
        val findViewWithoutGenericPattern = "((${include.layoutId}\\.)|(${include.layoutId}\\?.))findViewById\\(R\\.id\\.$it\\)(\\.|\\?.|\$)".toRegex()
        content = content.replace("((\\b$it\\?.)|(\\b$it\\.))".toRegex(), "mBinding.${include.bindingName}.${snakeToLowerCamelCase}.")
        content = content.replace("((itemView.$it\\?.)|(itemView.$it\\.)|(itemView.$it))".toRegex(), "mBinding.${include.bindingName}.${snakeToLowerCamelCase}.")
        content = content.replace("\\b$it\\s*as".toRegex(), "mBinding.${include.bindingName}.${snakeToLowerCamelCase} as")
        content = content.replace("\\b$it\\s*is".toRegex(), "mBinding.${include.bindingName}.${snakeToLowerCamelCase} is")
        content = content.replace(findViewGenericPattern,"mBinding.${include.bindingName}.$snakeToLowerCamelCase.")
        content = content.replace(findViewBlankGenericPattern,"mBinding.${include.bindingName}.$snakeToLowerCamelCase.")
        content = content.replace(findViewWithoutGenericPattern,"mBinding.${include.bindingName}.$snakeToLowerCamelCase.")
    }
    }
    layoutDetails.ids.forEach {
    val snakeToLowerCamelCase = it.snakeToLowerCamelCase()
    val findViewGenericPattern = "\\S.*findViewById<[^>]+>\\(R\\.id\\.$it\\)(\\.|\\?.|$)".toRegex()
    val findViewBlankGenericPattern = "\\S.*findViewById<>\\(R\\.id\\.$it\\)(\\.|\\?.|$)".toRegex()
    val findViewWithoutGenericPattern = "\\S.*findViewById\\(R\\.id\\.$it\\)(\\.|\\?.|$)".toRegex()
//        content = content.replace("((^itemView.$it\\?.)|(^itemView.$it\\.)|(^itemView.$it))".toRegex(), "mBinding.$snakeToLowerCamelCase.")
//        content = content.replace("((\\b$it\\?.)|(\\b$it\\.))".toRegex(), "mBinding.$snakeToLowerCamelCase.")
//        content = content.replace("\\b$it\\b".toRegex(), "mBinding.${snakeToLowerCamelCase}")
    val idToReplaceRegex =
        "(?<!mBinding\\.)\\b(?:itemView\\.$it|itemView\\?\\.$it|$it\\?|$it)\\b".toRegex()
    content = content.replace(idToReplaceRegex, "mBinding.$snakeToLowerCamelCase")
    content = content.replace("\\b$it\\s*is".toRegex(), "mBinding.$snakeToLowerCamelCase is")
    content = content.replace("\\b$it\\s*as".toRegex(), "mBinding.$snakeToLowerCamelCase as")
    content = content.replace(findViewGenericPattern,"mBinding.$snakeToLowerCamelCase.")
    content = content.replace(findViewBlankGenericPattern,"mBinding.$snakeToLowerCamelCase.")
    content = content.replace(findViewWithoutGenericPattern,"mBinding.$snakeToLowerCamelCase.")
    }
    Files.write(path, content.toByteArray(charset))
}

fun addBindingPlaceHolder(layoutDetails: LayoutDetails, filePath: String, className: String): List<String> {
    val path = Path.of(filePath)
    val readAllLines = Files.readAllLines(path)
    val fragRegex = Regex("onCreateView\\s*\\(")
    val fragDestroyViewRegex = Regex("fun\\s*onDestroyView\\s*\\(")
    println("\n**BINDING START ${layoutDetails.ktFileName}")
    //Custom view case
    if(isCustomView(readAllLines)){
    replaceLayoutInflaterLine(readAllLines[0], readAllLines, layoutDetails, true)
    }
    return readAllLines.map { line ->
    if (line.contains("setContentView(R.layout.")) {//Activity case
        println("CASE Activity")
        preparePlaceholderStringForActivity(line, layoutDetails)
    }else if(fragRegex.containsMatchIn(line)){  //Fragment/DialogFragment
        println("CASE Fragment/DialogFragments due to this line $line")
        replaceLayoutInflaterLine(line, readAllLines, layoutDetails, isNullableBinding = true)
        line
    }else if((line.contains("class ${className.replace(".kt", "")}")) &&
        !isViewHolderClass(line) && !layoutDetails.defineBinding
        && !isRnModule(readAllLines)){
        println("CASE Class start")
        val pair = findInNextLines(line, readAllLines, { list, index, lineAtIndex->
            try {
                Regex("\\s*\\{\\s*").containsMatchIn(lineAtIndex)
            }catch (e:StringIndexOutOfBoundsException){
                println("ERROR: readAllLines.size ${list.size} index is $index")
                false
            }
        })
        val currentLineIndex = readAllLines.indexOf(line)
        if(pair.first){
            readAllLines[pair.second] += "\n\t$defineBindingPlaceholder"
            layoutDetails.defineBinding = true
        }
        if(currentLineIndex == pair.second) readAllLines[pair.second] else line
    }else if(!layoutDetails.importBinding
        && line.contains("import kotlinx.android.synthetic.") && !isRnModule(readAllLines)){
        println("CASE Import start")
        layoutDetails.importBinding = true
        "import com.xyz.app.databinding.$importBindingPlaceholder"
    } else if(isViewHolderClass(line)){ //ViewHolder case
        handleViewHolderCase(line, readAllLines, layoutDetails)?:line
    }else if(line.contains("import kotlinx.android.synthetic.") && !line.contains(importBindingPlaceholder)){
        ""
    }else if(fragDestroyViewRegex.containsMatchIn(line) && layoutDetails.defineBinding){
        "$line\n_mBinding=null"
    }else {
        line
    }
    }
}

fun resolveAllIds(layoutName: String, layoutDetails: LayoutDetails) {
    var layoutPath = Path.of("$layoutDir$layoutName.xml")
    if(!Files.exists(layoutPath)){
    layoutPath = Path.of("$layoutV21Dir$layoutName.xml")
    if(!Files.exists(layoutPath)){
        println("ERROR: layout file doesn't exists $layoutPath")
        throw FileNotFoundException("")
    }
    }
    val readAllLines = Files.readAllLines(layoutPath)
    val ignoredIds = mutableListOf<String>()
    var layoutChanged = false
    readAllLines.forEach { line ->
    if(line.contains("<ViewStub")){
        println("\n**View stub started for ${layoutDetails.ktFileName}")
        val layoutChangedAndIgnoredIds =
            handleViewStubOrIncludeLayout(line, "ViewStub", layoutDetails, readAllLines)
        ignoredIds.addAll(layoutChangedAndIgnoredIds.second)
        layoutChanged = layoutChangedAndIgnoredIds.first
    }else if(line.contains("<include")){
        println("\n**include started for ${layoutDetails.ktFileName}")
        val layoutChangedAndIgnoredIds =
            handleViewStubOrIncludeLayout(line, "include", layoutDetails, readAllLines)
        ignoredIds.addAll(layoutChangedAndIgnoredIds.second)
        layoutChanged = layoutChangedAndIgnoredIds.first
    } else if(line.contains("android:id=\"@+id/")){
        val nonComplete = line.replace(Regex(".*@\\+id\\/"), "")
        val completeId = nonComplete.replace(Regex("\".*\$"), "")
        if(!ignoredIds.contains(completeId)){
            layoutDetails.ids.add(completeId)
        }
    }
    }
    if(layoutChanged){
    Files.write(layoutPath, readAllLines)
    }
}

fun handleViewStubOrIncludeLayout(
    line: String,
    viewStubOrInclude: String,
    layoutDetails: LayoutDetails,
    readAllLines: MutableList<String>
): Pair<Boolean,MutableList<String>> {
    val ignoredIds = mutableListOf<String>()
    val includeLayoutDetails = IncludeLayoutDetails()
    val idStatement = resolveIncludeOrViewStubId(line, readAllLines, viewStubOrInclude,
        includeLayoutDetails, layoutDetails)
    val layoutChanged = resolveIncludeOrViewStubLayout(
    line, readAllLines, viewStubOrInclude, idStatement,
    includeLayoutDetails, ignoredIds, layoutDetails
    )
    return Pair(layoutChanged,ignoredIds)
}

fun preparePlaceholderStringForActivity(line: String, layoutDetails: LayoutDetails): String {
    val layoutName = resolveActivityLayoutName(line).trim()
    layoutDetails.layoutName = layoutName
    layoutDetails.bindingName = layoutName.snakeToUpperCamelCase() + "Binding"
    return "\t\tmBinding = ${layoutDetails.bindingName}.inflate(layoutInflater)\n\t\tsetContentView(mBinding.root)"
}

fun preparePlaceholderStringForFragment(
    line: String,
    layoutDetails: LayoutDetails,
    useLayoutInflater: Boolean = false,
    context: String? = null,
    customView: Boolean = false,
    viewHolder: Boolean = false,
    isNullableBinding: Boolean
): String {
    val layoutName = resolveFragmentLayoutName(line).trim()
    println("Layout name for ${layoutDetails.ktFileName}  is $layoutName")
    layoutDetails.layoutName = layoutName
    layoutDetails.bindingName = layoutName.snakeToUpperCamelCase() + "Binding"
    val container = line.replace(Regex(".*layout[.]"), "").replace(")", "")
    val split = container.split(",")
    val parent = split.getOrNull(1)?.trim()?:"null"
    val attachToRoot = split.getOrNull(2)?.trim()?:(parent != "null")
    val inflater = if(useLayoutInflater) "LayoutInflater.from($context)" else "inflater"
    val bindingDeclaration =
    "\n\t\t${if(viewHolder) "val " else ""}" +
            "${if(isNullableBinding) "_mBinding " else "mBinding"} = " +
            "${layoutDetails.bindingName}.inflate($inflater, $parent, $attachToRoot)"
    val returnView = "\n\t\treturn view"
    val getView = "\n\t\tval view = mBinding.root"
    val stringBuilder =
    StringBuilder(bindingDeclaration)
    if(customView.not() && viewHolder.not()){
    stringBuilder.append(getView)
        .append(returnView)
    }
    return (stringBuilder.toString()).also {
    println("Binding placeholder is $it")
    }
}

fun findInNextLines(line: String,
                readAllLines: MutableList<String>,
                condition:(MutableList<String>, Int, String)->Boolean,
                tillIndex :Int? = 10,
                breakingDelimeter:String? = null): Pair<Boolean, Int> {
    var index = readAllLines.indexOf(line)
    val breakingLoop = index + (tillIndex?.let { tillIndex.coerceAtMost(readAllLines.size - index - 1) }?:0)
    while (index > -1 && index < readAllLines.size) {
    val lineAtIndex = readAllLines[index]
    if (condition.invoke(readAllLines, index, lineAtIndex)) {
        return Pair(true, index)
    }
    if(!breakingDelimeter.isNullOrEmpty() && lineAtIndex.contains(breakingDelimeter)){
        return Pair(false, index)
    }
    if(index >= breakingLoop){
        return Pair(false, index)
    }
    index++
    }
    return Pair(false, --index)
}

fun replaceLayoutInflaterLine(
    line: String,
    readAllLines: MutableList<String>,
    layoutDetails: LayoutDetails,
    isCustomView:Boolean = false,
    isViewHolder:Boolean = false,
    isNullableBinding:Boolean = false) {
    val layoutInflaterIndex = readAllLines.indexOfFirst {
    it.contains("LayoutInflater.from") || it.contains("inflater.")
            || it.contains("layoutInflater.")
    }
    if (layoutInflaterIndex > -1) {
    val currentLine = readAllLines[layoutInflaterIndex]
    println("either LayoutInflater.from or inflater found in $currentLine")
    if (currentLine.contains("LayoutInflater.from")) {
        println("exists LayoutInflater.from")
        val context = currentLine.replace(Regex(".*from\\("), "").customReplaceAfter(").", "").replace(")","")
        if ((currentLine.contains(".inflate(layout.") || currentLine.contains(".inflate(R.layout."))) { //No new line case
            readAllLines[layoutInflaterIndex] =
                preparePlaceholderStringForFragment(
                    readAllLines[layoutInflaterIndex],
                    layoutDetails, true,
                    context, isCustomView, isViewHolder,
                    isNullableBinding)
        } else { //New line case
            val inflateStatement = findInNextLines(currentLine, readAllLines, { list, index, lineAtIndex ->
                try {
                    (lineAtIndex.contains(".inflate(layout.") || lineAtIndex.contains(".inflate(R.layout."))
                } catch (e: StringIndexOutOfBoundsException) {
                    println("ERROR: readAllLines.size ${list.size} index is $index")
                    false
                }
            })
            if (inflateStatement.first) {
                try {
                    readAllLines[layoutInflaterIndex] = ""
                    readAllLines[inflateStatement.second] =
                        preparePlaceholderStringForFragment(
                            readAllLines[inflateStatement.second],
                            layoutDetails,
                            true,
                            context,
                            isCustomView,
                            isViewHolder,
                            isNullableBinding
                        )
                } catch (e: StringIndexOutOfBoundsException) {
                    println("ERROR: readAllLines.size ${readAllLines.size} index is ${inflateStatement.second}")
                }
            } else {
                println("ERROR: Couldn't find inflater for ${layoutDetails.ktFileName}")
            }
        }
    } else if (currentLine.contains("inflater.") || currentLine.contains("layoutInflater.")) {
        println("exists inflater.")
        if ((currentLine.contains(".inflate(layout.") || currentLine.contains(".inflate(R.layout."))) { //No new line case
            try {
                readAllLines[layoutInflaterIndex] =
                    preparePlaceholderStringForFragment(
                        readAllLines[layoutInflaterIndex],
                        layoutDetails,
                        false,
                        null,
                        isCustomView,
                        isViewHolder,
                        isNullableBinding)
            } catch (e: StringIndexOutOfBoundsException) {
                println("ERROR: readAllLines.size ${readAllLines.size} index is $layoutInflaterIndex")
            }

        } else { //New line case
            val inflateStatement = findInNextLines(currentLine, readAllLines, { list, index, lineAtIndex ->
                try {
                    (lineAtIndex.contains(".inflate(layout.") || lineAtIndex.contains(".inflate(R.layout."))
                } catch (e: StringIndexOutOfBoundsException) {
                    println("ERROR: readAllLines.size ${list.size} index is $index")
                    false
                }
            })
            if (inflateStatement.first) {
                try {
                    readAllLines[layoutInflaterIndex] = ""
                    readAllLines[inflateStatement.second] +=
                        preparePlaceholderStringForFragment(
                            readAllLines[inflateStatement.second],
                            layoutDetails,
                            false,
                            null,
                            isCustomView,
                            isViewHolder,
                            isNullableBinding
                        )
                } catch (e: StringIndexOutOfBoundsException) {
                    println("ERROR: readAllLines.size ${readAllLines.size} index is ${inflateStatement.second}")
                }
            } else {
                println("ERROR: Couldn't find inflater for ${layoutDetails.ktFileName}")
            }
        }
    }
    }else{
    println("Couldn't find inflater")
    }
}

fun handleViewHolderCase(
    line: String,
    readAllLines: MutableList<String>,
    layoutDetails: LayoutDetails
) : String? {
    try {
    println("CASE ViewHolder due to this line $line")
    val viewHolderClassName = Regex("class\\s+([A-Za-z_][A-Za-z0-9_]*)").find(line)?.value
        ?.replace("class", "")?.trim()
    println("ViewHolder class name $viewHolderClassName")
    val fromFunRegex = Regex("fun from\\(*.*\\)\\s*:|=\\s*$viewHolderClassName")
    val fromFunStatementPair = findInNextLines(line, readAllLines, { list, index, lineAtIndex ->
        fromFunRegex.containsMatchIn(lineAtIndex)
    }, 50)
    if (fromFunStatementPair.first) {
        replaceLayoutInflaterLine(readAllLines[fromFunStatementPair.second],readAllLines,
            layoutDetails, isViewHolder = true)
        if (layoutDetails.layoutName.isNullOrEmpty().not()) {
            println("Binding added success for view holder ${layoutDetails.ktFileName}")
        } else {
            println("Binding added failed for view holder ${layoutDetails.ktFileName}")
        }
    } else {
        println("Couldn't find from function in Viewholder class")
    }
    val returnStatementPair = findInNextLines(readAllLines[fromFunStatementPair.second],
        readAllLines,{ _, _, lineAtIndex ->
            Regex("return\\s*$viewHolderClassName\\s*\\(").containsMatchIn(lineAtIndex)
        })
    if (returnStatementPair.first) {
        readAllLines[returnStatementPair.second] = "return $viewHolderClassName(mBinding)"
    } else {
        println("Return statement couldn't find in Viewholder class")
    }
    val constructorParams = Regex("\\(([^)]+)\\)").find(line)?.value?.replace("(", "")
        ?.replace(")", "")
    println("constructorParams $constructorParams")
    val completeViewParam = Regex("\\(([^()][^()]*)\\)[^()]*\$").find(line)?.value
        ?.replace("(", "")?.replace(")", "")?.replace("{", "")?.trim()
    println("completeViewParams $completeViewParam")
    if(!constructorParams.isNullOrEmpty() && !completeViewParam.isNullOrEmpty()){
        val finalConstructorParams = constructorParams.replace(Regex("$completeViewParam\\s*:\\s*\\w+"),
            "private val mBinding: ${layoutDetails.bindingName}")
        println("finalConstructorParams $finalConstructorParams")
        if(constructorParams.isNotEmpty() && finalConstructorParams.isNotEmpty())
            return line.replace(constructorParams, finalConstructorParams)
                .replace(completeViewParam, "mBinding.root")
                .also {
                println("Final Constructor line is $it")
            }
    }
    } catch (e: Exception) {
    println("ERROR: ${e.message}")
    }
    return null
}

fun resolveIncludeOrViewStubId(line: String,readAllLines: MutableList<String>,
                           viewStubOrInclude: String,includeLayoutDetails: IncludeLayoutDetails,
                           layoutDetails: LayoutDetails): Pair<Boolean, Int> {
    val idStatement = findInNextLines(line, readAllLines, { list, index, lineAtIndex ->
    lineAtIndex.contains("<$viewStubOrInclude android:id=\"@+id")
            || lineAtIndex.contains("android:id=\"@+id")
    }, breakingDelimeter = "/>")
    if (idStatement.first) {
    val idLine = readAllLines[idStatement.second]
    val nonComplete = idLine.replace(Regex(".*@\\+id\\/"), "")
    val layoutId = nonComplete.replace(Regex("\".*\$"), "")
    includeLayoutDetails.layoutId = layoutId
    includeLayoutDetails.bindingName = layoutId.snakeToLowerCamelCase()
    println("$viewStubOrInclude id got resolved ${includeLayoutDetails.layoutId} and binding name ${includeLayoutDetails.bindingName}")
    } else {
    println("$viewStubOrInclude id could not get resolved for ${layoutDetails.ktFileName}")
    }
    return idStatement
}

fun resolveIncludeOrViewStubLayout(line: String,readAllLines: MutableList<String>,
    viewStubOrInclude: String,idStatement: Pair<Boolean, Int>,includeLayoutDetails: IncludeLayoutDetails,
    ignoredIds: MutableList<String>,layoutDetails: LayoutDetails): Boolean {

    val layoutStatement = findInNextLines(line, readAllLines, { _, _, lineAtIndex ->
    (lineAtIndex.contains("android:layout=\"@layout/")
            || lineAtIndex.contains("layout=\"@layout/")) && !lineAtIndex.contains("<!--")
    }, breakingDelimeter = "/>")
    var idAdded = false
    if (layoutStatement.first) {
    val layoutLine = readAllLines[layoutStatement.second]
    val nonComplete = layoutLine.replace(Regex(".*@layout\\/"), "")
    val layoutName = nonComplete.replace(Regex("\".*\$"), "")
    println("$viewStubOrInclude layoutName got resolved $layoutName")
    if (!idStatement.first) {
        val indexOfIncludeTag = readAllLines.indexOf(line)
        if (indexOfIncludeTag >= 0) {
            var copyLayoutName = layoutName
            if (!copyLayoutName.contains("include")) {
                copyLayoutName = "include_$copyLayoutName"
            }
            val includeId = copyLayoutName.snakeToLowerCamelCase()
            if(line.contains("/>")){
                readAllLines[indexOfIncludeTag] = line.replace("/>", "\nandroid:id=\"@+id/$includeId\"/>")
            }else{
                readAllLines[indexOfIncludeTag] = "$line\n\tandroid:id=\"@+id/$includeId\""
            }
            idAdded = true
            includeLayoutDetails.layoutId = includeId
            includeLayoutDetails.bindingName = includeId.snakeToLowerCamelCase()
            println("Added $viewStubOrInclude id final include line is ${readAllLines[indexOfIncludeTag]}")
        }
    }
    var layoutPath = "$layoutDir$layoutName.xml"
    if (!Files.exists(Path.of(layoutPath))) {
        layoutPath = "$layoutV21Dir$layoutName.xml"
        if (!Files.exists(Path.of(layoutPath))) {
            println("ERROR: layout file doesn't exists $layoutDir$layoutName.xml")
            throw FileNotFoundException("")
        }
    }
    includeLayoutDetails.layoutName = layoutName
    includeLayoutDetails.layoutPath = layoutPath
    println("$viewStubOrInclude layoutPath got resolved $layoutPath")
    Files.readAllLines(Path.of(layoutPath)).forEach { includeLayoutLine ->
        if (includeLayoutLine.contains("android:id=\"@+id/")) {
            val nonCompleteId = includeLayoutLine.replace(Regex(".*@\\+id\\/"), "")
            val completeIncludeId = nonCompleteId.replace(Regex("\".*\$"), "")
            includeLayoutDetails.ids.add(completeIncludeId)
            ignoredIds.add(completeIncludeId)
        }
    }
    }
    println("viewStubDetails for ${layoutDetails.ktFileName} is $includeLayoutDetails")
    if (viewStubOrInclude == "ViewStub")
    layoutDetails.viewStubLayoutDetail.add(includeLayoutDetails)
    else if (viewStubOrInclude == "include")
    layoutDetails.includeLayoutDetail.add(includeLayoutDetails)

    return idAdded
}

fun resolveActivityLayoutName(it: String): String {
    val nonComplete = it.replace("setContentView(R.layout.", "")
    return nonComplete.replace(")", "")
}

fun resolveFragmentLayoutName(it: String): String {
    val nonComplete = it.replace(Regex(".*layout[.]"), "")
    return nonComplete.replace(Regex(",.*."), "")
    .replace("[^\\w\\s]+$".toRegex(), "")
}

fun isViewHolderClass(line: String): Boolean {
    return "(class\\s+[A-Za-z_][A-Za-z0-9_]*ViewHolder\\b)|(:\\s+[A-Za-z_][A-Za-z0-9_]*ViewHolder\\()".toRegex().containsMatchIn(line)
}

fun isCustomView(lines: MutableList<String>): Boolean {
    val indexOfFirst = lines.indexOfFirst {
    it.contains("package com.xyz.app.customviews")
            || "\\)\\s*:\\s*FrameLayout\\s*\\(".toRegex().containsMatchIn(it)
    }
    return (indexOfFirst >= 0).also {
    if(it)
        println("Custom view Matched with line ${lines[indexOfFirst]}")
    }
}

fun isRnModule(lines: MutableList<String>):Boolean{
    val indexOfFirst = lines.indexOfFirst {
    it.contains("com.xyz.app.reactnative.modules")
    }
    return (indexOfFirst >= 0).also {
    if(it)
        println("RN module Matched with line ${lines[indexOfFirst]}")
    }
}

fun String.customReplaceAfter(delimiter: String, replacement: String, missingDelimiterValue: String = this): String {
    val index = indexOf(delimiter)
    return if (index == -1) missingDelimiterValue else replaceRange(index, length, replacement)
}

fun String.snakeToUpperCamelCase(): String {
    return snakeToLowerCamelCase()
    .replaceFirstChar { if (it.isLowerCase()) it.uppercase(Locale.getDefault()) else it.toString() }
}

fun String.snakeToLowerCamelCase(): String {
    return snakeRegex.replace(this) {
    it.value.replace("_","")
        .uppercase(Locale.getDefault())
    }
}