Welcome, and thank you for contributing! 🎉
Readable > Useful > High performance but poorly readable.
Truly readable code is more than just clear—it's understandable even without context (contextless readability).
-
Use English for all comments.
-
Stay polite in code comments. Avoid offensive language.
- You can be grumpy, but express it with decent wording.
-
Avoid confusing abbreviations. A reader unfamiliar with the abbreviation has to guess or grep; the few bytes saved are not worth the cognitive cost.
Bad:
dl := &net.Dialer{}
Good:
dialer := &net.Dialer{}
-
Redundant comments are as useless as this sentence.
-
Strive for readability through naming, not excessive comments.
-
Use constants wherever possible. Magic literals hide intent and scatter duplicates across the codebase; a named constant is self-documenting and has exactly one place to update.
Exceptions — inline literals are fine when the value is local and self-evident:
- Layout-only dp values in Compose UI that no other component references (e.g.
Modifier.padding(16.dp)). - Arbitrary delays with no domain meaning (e.g. a short animation debounce).
Bad:
import ( "net" N "github.com/sagernet/sing/common/network" ) func dnsConn() (net.Conn, error) { return net.Dial(N.NetworkUDP, "8.8.8.8:53") // Google DNS }
Good:
import ( "net" N "github.com/sagernet/sing/common/network" ) func dnsConn() (net.Conn, error) { const googleDNS = "8.8.8.8:53" return net.Dial(N.NetworkUDP, googleDNS) }
- Layout-only dp values in Compose UI that no other component references (e.g.
-
Our style uses names to communicate meaning.
-
Do not build filesystem paths by string concatenation such as
base + "/child"orabsolutePath + "/". String concatenation breaks on Windows (\vs/), mishandles trailing separators, and bypassesFile's normalization. -
Prefer
File.resolve(...),File(parent, child), or equivalent path APIs when combining local paths. -
On Windows, we should use
/instead of\. The Windows JVM returns\separators, but sing-box (the Go core) always expects/;invariantPathString()ensures the forward-slash form.Bad:
val geoDir = repository.externalAssetsDir.absolutePath + "/geo" ruleSet.path = "$geoDir/$name.srs"
Good:
import fr.husi.ktx.invariantPathString val geoDir = repository.externalAssetsDir.resolve("geo") ruleSet.path = geoDir.resolve("$name.srs").invariantPathString()
- Run
make fmt_goandmake test_gobefore committing. - Write unit tests wherever possible.
- Make documentation writing a habit.
- Always use imports instead of fully qualified names in code. Fully qualified names clutter the call site and make refactoring harder -- the IDE cannot collapse or auto-update them.
- The only exception is when referencing
Rclasses from other packages (e.g.,com.google.android.material.R).
Bad:
val density = androidx.compose.ui.platform.LocalDensity.current
androidx.compose.runtime.DisposableEffect(view) { /* ... */ }Good:
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.runtime.DisposableEffect
val density = LocalDensity.current
DisposableEffect(view) { /* ... */ }- Prefer explicit backing fields for public read-only
StateFloworSharedFlowproperties backed by mutable flows. - This keeps the public type read-only while avoiding extra
_uiState/_uiEventproperties. The_foo/foonaming dance leaks the mutable handle into the class's namespace; an explicit backing field scopes it to the property body, so nothing else in the class can accidentally mutate it.
Good:
val uiState: StateFlow<ScreenUiState>
field = MutableStateFlow(ScreenUiState())
val uiEvent: SharedFlow<ScreenUiEvent>
field = MutableSharedFlow<ScreenUiEvent>()
fun updateName(name: String) {
uiState.update { it.copy(name = name) }
}-
forEachis fluent, especially at the end of a chain. However,forEachcannotbreakorreturnfrom the enclosing function, so standalone iterations that may exit early should usefor.strings.filter { it.isNotEmpty() }.forEach { println(it) } -
For standalone iterations,
forloops are often more flexible:- Can use
break - Can use
returnfrom enclosing function - Explicit variable names are clearer
fun firstNonEmptyString(strings: List<String>): String? { for (string in strings) { if (string.isNotEmpty()) { return string } } return null }
- Can use
- Prefer
alsooverapplywhenthisis ambiguous. Insideapply,thisrebinds to the receiver -- in a class that already has properties with the same names, the wrong one shadows silently, causing bugs that compile without warning. applyis great for object configuration, but nested scopes (e.g. in Activities or Fragments) may introduce confusion.alsomakes the receiver explicit viait, improving readability.
Example of ambiguity with apply:
private lateinit var textView: TextView
private val isVisible = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView = findViewByID(R.id.textView).apply {
this@apply.isVisible = isVisible // `this` is ambiguous
}
}Preferred version with also:
private lateinit var textView: TextView
private val isVisible = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView = findViewByID(R.id.textView).also {
it.isVisible = isVisible // `it` clearly refers to the TextView
}
}