AI refac

بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ

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:name and app: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, and android:manageSpaceActivity. In the manifest a bare MainActivity also 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 from kotlin = "x.y.z" in gradle/libs.versions.toml, else from the kotlin("multiplatform") version "x.y.z" line or a kotlin-gradle-plugin:x.y.z classpath 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 expect or actual are 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 (commonMain to jvmMain) are refused.
  • Two source sets, one name. Names defined in two source sets of one package, such as an actual class in androidMain and in desktopMain, 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.

On this page