بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Android and Multiplatform
What refac adds for Android projects after the Kotlin server is done, and how it handles Kotlin Multiplatform builds.
The Kotlin language server moves and renames code, but it never touches Android XML, and a file that leaves its module's namespace package loses the implicit R and BuildConfig it used. refac repairs both after the server is done, for refac move and for refac rename. A project without an Android module passes through untouched.
This page explains what the Android layer does, what it leaves to you, and how a Multiplatform build is served.
Android
Which module is Android
A module is Android when its build.gradle(.kts) sets namespace = "..." (Kotlin DSL) or namespace '...' (Groovy), or when src/<source set>/AndroidManifest.xml exists next to it. A module that has a manifest but no readable namespace is an error that names the build script, because relative class names and generated classes could not be handled correctly. A root script that merely mentions the Android plugin does not make a module. The deepest module around a file owns it.
Class names in XML
When classes change their fully qualified name, because of a move to another package or a rename, refac rewrites them in AndroidManifest.xml and in every src/<source set>/res/**/*.xml of the owning module:
- Element names that are classes, such as
<com.example.widgets.BadgeView>. - Attribute values that are fully qualified class names, such as navigation
android:nameandapp:argType. - Names relative to the namespace, in the attributes that allow them:
android:name,tools:context,android:targetActivity,android:parentActivityName,android:fragment,app:fragment,class,android:backupAgent, andandroid:manageSpaceActivity. In the manifest a bareMainActivityalso means<namespace>.MainActivity.
A relative name stays relative while the class stays under the namespace (.MainActivity becomes .ui.MainActivity) and becomes fully qualified when it leaves. Only the changed names are replaced, in place, so formatting, comments, and line endings survive, and commented-out XML is left alone. XML that cannot be read safely, such as an unterminated tag or an unquoted attribute, is an error and nothing is written.
R and BuildConfig
Code in the namespace package uses R and BuildConfig without an import. A file that moves out of that package and uses either one unqualified gets import <namespace>.R or import <namespace>.BuildConfig. The import goes in sorted position when the existing imports are sorted, and after the last import otherwise, with the file's line endings. An existing import of the same name, for example android.R, is respected. Uses such as android.R.id.home and text in comments do not count.
Old names refac cannot rewrite
After the edits, refac scans the files it does not edit for the old fully qualified class names, including the FileKt facade class of a moved file that holds top-level functions. It looks at ProGuard rules, build scripts (mainClass), service lists, configuration (properties, json, yaml, toml), XML it did not change, and string literals in sources such as reflection and Class.forName. Each hit becomes a note with the file, the old name, and the new name. The scan only reports and changes nothing, and it reads what .gitignore leaves in, so a reference clone or a scratch folder in the same repository is not part of the project.
Not covered
- Resource ids and resource files (
R.string.x, layout names) are never renamed. - View binding classes follow layout names and not Kotlin classes, so a moved activity needs no binding change.
- Multi-module Android projects with several libraries were not tested. The logic is per module, but treat the first such move as something to review.
Kotlin Multiplatform
The Kotlin language server cannot read a Kotlin Multiplatform Gradle build. Its import ends without an error but prints Failed to find 'target' in Kotlin extension, no source set is known afterwards, and every move is refused. The server does read a plain Kotlin/JVM build, so refac gives it one.
What refac does
When a Gradle module has a source set named commonMain, commonTest, or <target>Main (jvmMain, androidMain, iosMain, desktopMain), refac starts the server on a temporary plain-JVM copy of the project and translates paths in both directions. The copy holds the .kt and .java files of every source set and the Gradle wrapper files, laid out as one kotlin("jvm") module. It is deleted when the command ends. Moves, renames, the Android layer, the rollback, and the dry run all keep speaking in the real project's paths. Android and plain JVM modules use main, test, and the names of variants and flavors, so they never get a copy.
Package lines and references to the project's own classes follow, also inside the lambda of a library call. Imports of libraries that the copy cannot see, such as Compose and Android, are kept: the server would delete them as unused, and refac puts them back.
What you need
- The Kotlin version of the copy. It comes from
REFAC_KOTLIN_MIRROR_VERSION, else fromkotlin = "x.y.z"ingradle/libs.versions.toml, else from thekotlin("multiplatform") version "x.y.z"line or akotlin-gradle-plugin:x.y.zclasspath entry of a build script. A version that cannot be read is an error naming the variable. - The Kotlin Gradle plugin of that version, from the plugin portal or Maven Central. A build that can only reach an internal repository needs the plugin in the Gradle cache of the machine.
Limits
- Files that declare
expectoractualare refused before the server starts, and the error names the file and the line. In the copy an expect declaration and its actual are two declarations of one name, and moving one side alone would leave the other in a package it must share. Move those files by hand: change the package line, and the compiler lists the imports that follow. A rename of a symbol that such a file declares is refused the same way. - Moves between source sets (
commonMaintojvmMain) are refused. - Two source sets, one name. Names defined in two source sets of one package, such as an
actual classinandroidMainand indesktopMain, are one name to the server, and its edits do not tell the two apart. Compile both targets afterwards. - What was tested. A small fixture with a common and a JVM target, and a hand-over from a Compose Multiplatform application. A full application with Android, desktop, and iOS targets was not tried here. Compile every target after a change, and they should inshallah all build.
Rename
Rename a Kotlin symbol and every reference to it, Java callers and Android XML included, with a proof that the program keeps its meaning.
Cost and Troubleshooting
Why a Kotlin change is slow, what to do when a single change is refused, and how to fix the server, memory, timeout, and Gradle problems.