Rules for code of Telegram X, for people and AI agents alike. Formatting settings are in .editorconfig, which Android Studio applies. New code follows these rules even where older code around it doesn't.
Indentation is 2 spaces, lines are at most 100 characters. Only string literals and links that can't be split are longer.
A declaration that doesn't fit into one line has one parameter per line, and the closing parenthesis on its own line:
// Don't
private class TestToolsRun(val name: String, val step: String, val code: Int, val seconds: String, val command: String)
// Do
private class TestToolsRun(
val name: String,
val step: String,
val code: Int,
val seconds: String,
val command: String
)An expression body after = starts in the same line when the declaration fits into one line, even when the body takes several lines. = ends the line, and the body is indented by 2 spaces, after parameters on separate lines, when the first line would be longer than 100 characters, or when the body is a call with arguments on separate lines and without a trailing lambda:
// Don't
fun collect(
outputFilePrefix: String,
block: MacrobenchmarkScope.() -> Unit
) = rule.collect(
outputFilePrefix = outputFilePrefix,
packageName = getApplicationId()
) {
pressHome()
block()
}
// Do
fun collect(
outputFilePrefix: String,
block: MacrobenchmarkScope.() -> Unit
) =
rule.collect(
outputFilePrefix = outputFilePrefix,
packageName = getApplicationId()
) {
pressHome()
block()
}
// Do
fun launchDefault() = collect(
"launch_authorized",
includeInStartupProfile = true
) {
startActivityAndWait()
}
// Do, when the first line would be too long
private fun describeImplementationOfOperation(implementation: String, operation: String) =
when (implementation) {
// ...
}
// Do, a call without a trailing lambda
fun drawsWithoutAllocations() =
assertDrawsWithoutAllocations(
view,
width = 200,
height = 100
)A call that doesn't fit into one line has one argument per line, and a trailing lambda follows the closing parenthesis.
// Don't
runOnFrames(SKIPPED_FRAMES + frames, beforeFrame = { frameAllocations = Debug.getThreadAllocCount() }) { frame ->
// Do
runOnFrames(
SKIPPED_FRAMES + frames,
beforeFrame = {
frameAllocations = Debug.getThreadAllocCount()
}
) { frame ->A Kotlin lambda has its body on separate lines, unless the body is one expression without spaces outside string literals, the lambda is the only one passed to the call, and the line stays short: files.filter { it.isFile }, Array(100) { "Chat $it" }. A call after the closing brace of a lambda goes to the next line, like other calls of a chain:
// Don't
val savedBefore = savedRuns.of(baseline).filter { it.id != current.id }
val worst = rows.filter { it.second[index].startsWith(WORST) }.map { it.first }
// Do
val savedBefore = savedRuns.of(baseline).filter {
it.id != current.id
}
val worst = rows
.filter { it.second[index].startsWith(WORST) }
.map { it.first }A chain of calls that doesn't fit into one line has one call per line, and a long condition breaks after && or ||. A named value often makes a long expression shorter and clearer:
// Don't
assertWithMessage("Allocations caught in wrong scrolling").that(counted.allocations).isGreaterThan(0)
// Do
assertWithMessage("Allocations caught in wrong scrolling")
.that(counted.allocations)
.isGreaterThan(0)Bodies of if, else, for and while always have braces and line breaks, even when they are one statement. Short Kotlin functions and if expressions that choose a value stay in one line: val mark = if (isTarget) "*" else " ".
// Don't
if (text == null) return;
// Do
if (text == null) {
return;
}Java declarations of methods have a space before parameters, calls don't: public static int dp (float size), Screen.dp(16f). Kotlin has no space in both.
Kotlin has no trailing commas.
Names are descriptive and not abbreviated: destinationDir, not dstDir.
Comments say only what code can't: a workaround of a bug of Android or a library, the reason of an unusual decision, an algorithm that the name of the function doesn't explain, a contract that callers rely on. They never restate code.
// Don't
// Posts the callback
choreographer.postFrameCallbackDelayed(this, -BEFORE_FRAME_DELAY_MS)
// Do
// Callbacks of a frame run in order of time when they are due,
// so callback due earlier than posted runs first
choreographer.postFrameCallbackDelayed(this, -BEFORE_FRAME_DELAY_MS)New code adds no warnings. A warning that can't be fixed is suppressed on the smallest declaration that causes it, with a comment when the reason isn't obvious:
// Don't: warning 'getIsolated()' is marked unstable with @Incubating
private fun Project.findAppKeystore(): Keystore? {
val root = isolated.rootProject.projectDirectory
// Do
@Suppress("UnstableApiUsage")
private fun Project.findAppKeystore(): Keystore? {
val root = isolated.rootProject.projectDirectory- Changes are minimal and reuse existing code: helpers of the module, utilities of
me.vkryl, components of the app. Repeated code is extracted into a short function. - Code of other projects is never copied: repositories are added as git submodules, libraries as Gradle dependencies.
- Kotlin code reads Java getters as properties:
it.type.constructor, notit.type.getConstructor(). - Kotlin is allowed in new modules outside of
org.thunderdog(seeme.vkrylpackage), tests and build code, as long as it is interoperable with Java code or private. - New files in
appstart with the license header, copied from a neighbouring file. - Build code in
buildSrcandtests/buildSrcfails withfatal(message)oftgx.gradle. - Drawing, animations and scrolling don't allocate objects or request layout on frames, see tests.
- Icons are vector drawables only. Their viewport (
viewportWidth,viewportHeight) is 24x24, display size is set bywidthandheightand is the suffix of the file name, e.g.baseline_access_time_24.xml.