The Kotlin coding specification used in a big BAT factory has added a lot of contents on the basis of the official Kotlin specification, which is of great reference value.
Code organization
-
[mandatory] in a mixed java source project, the Kotlin source file should be located in the same root directory as the Java source file, instead of being placed separately according to the file type
-
Force: if the source file contains only a single class, the class name will be used as the file name
-
[mandatory] the contents of a class are listed in the following order (PS: it is officially recommended that the associated objects be placed at the end, but there are also case s at the top in the source code, but not in the middle)
-Attribute declaration and initialization block -Secondary constructor -Method declaration -Companion object
-
[force] when implementing an interface, the order of implementing members should be the same as that of the interface
Two naming conventions
- [mandatory] package name is lowercase and underline is not allowed
import com.intellij.openapi.action_system // Counterexample
- [mandatory] the name between the separator of package name points should be as short as possible, avoiding the use of multiple word names. If it is really necessary to use multiple words, connect the lowercase letters together, and avoid the use of hump names
import org.example.myproject // Positive example import org.example.myProject // Counterexample
- [mandatory] the name of the object declaration (singleton class) is the same as that of the ordinary class, starting with an uppercase letter and using the hump
object Comparator {...}
- [mandatory] generic types are represented in all capital letters
// Counter example: generics are not fully capitalized class Box<Type>(t: Type) { var value = t }
- [mandatory] use names separated by uppercase and underline to make sure the meaning is clear, and don't worry about the length of names
[description] constant includes const attribute or top-level val attribute Type
[exception] except for objects with behavior or customer getter
const val MAX_COUNT = 8 val USER_NAME_MAP = mapOf("UserName", ...)
- [mandatory] enumeration constant names separated by uppercase and underscore
enum class Color { RED, GREEN }
Three code format
- [mandatory] if the class / interface has no body content, the curly bracket is omitted
class EmptyClass {} // Counterexample interface EmptyInterface {} // Counterexample object EmptyObject {} // Counterexample
- Force: for a function body composed of a single expression, the expression form is preferred.
fun foo() = 1 // Positive example fun foo(): Int { return 1 } // Counterexample
- Force: if the user-defined get and set methods contain code blocks, you need to write get, set, and newline
// Simple get, no line wrapping val isEmpty: Boolean get() = size == 0 // get has code block newline val foo: String get() { /*...*/ }
- Force the when statement places the short branch on the same line as the condition without curly braces.
// Example: when (foo) { true -> bar() false -> baz() } // Counter example: when (foo) { true -> { bar() } false -> { baz() } }
- When a method is defined by force, if there is a lambda expression, it will be the last parameter. If there are more than one, try to choose a lambda with the highest priority.
// Example: fun <T, C: MutableCollection<in T>> List<T>.filterTo(destination: C, predicate: (T) -> Boolean): C // Counter example: fun <T, C: MutableCollection<in T>> List<T>.filterTo(predicate: (T) -> Boolean, destination: C): C
4. Idiomatic grammar
-
Forced: non empty type is used by default when variables are declared
-
[mandatory] can ensure that when used, it must be a non empty member, using lateinit instead of nullable type.
Note: Activity recovery and reconstruction may affect the initialization process. At this time, the lateinit variable should not be empty -
[mandatory] use val instead of var as much as possible
-
[force] minimize the open declaration of classes and members
-
[force] use List/Set/Map instead of MutableList/MutableSet/MutableMap as much as possible
-
When multiple variables are forced to be connected, the string template is used
val fullName = "${user.firstName} ${user.lastName}" //Positive example val fullName = user.firstName + " " + user.lastName // Counterexample
- Force single line functions to omit braces, or when the return value type is clear
fun foo(a: Int, b: Int) = a + b // Positive example fun foo(a: Int, b: Int): Int { return a + b } // Counterexample
- When the last parameter of the forced call function is the function type, omit the parenthesis
observable.subscribe({ /*...*/ }) // Counterexample observable.subscribe { /*...*/ } // Positive example
- [mandatory] use = = replace equals
if (this::class.java.equals(clazz)) // Counterexample if (this::class.java == clazz) // Positive example
- Forced: no type conversion is required when using variables that have been type checked
if (x is String) { print(x.length) } // Positive example if (x is String) { print((x as String).length) } // Counterexample
- [mandatory] run time type check through as
println((s as? Int) ?: 0) // Positive example if (s is Int) println(s) else println(0) // Counterexample
-
[force] you need to add else at the end of the when statement to prevent logical errors
[exception] a sealed class only needs to list all subclasses -
Force: use expression statements to avoid multiple return s in if statements
// Example: fun isPass(score: Score) : Boolean { return if (score >= 60) { ... true } else { ... false } } // Counter example: fun isPass(score: Score) : Boolean { if ( score >= 60) { ... return true } else { .. return false } }
- [force] use until instead of size - 1 in for loop
// Counter example: for (i in 0..(list.size - 1)) { ... } // Example: for (i in 0 until list.size) { ... }
- Force: use the scope function when reading / writing the same variable continuously
[note] you need to access the nullable variable several times continuously. It will be judged every time. However, when you use the?. + scope function, all the variables in the scope are final and non empty, reducing the number of empty judgments.
val member: User? // Counter example: fun initView(){ tvName.text = member?.name tvAge.text = member?.age member?.lastAccessedTime = System.currentTimeMillis() button.setOnClickListener{ member?.selected = !member?.selected } } // Example: fun initView(){ member?.apply{ tvName.text = name tvAge.text = age lastAccessedTime = System.currentTimeMillis() } button.setOnClickListener{ member?.apply{ selected = !selected} } }
Keep the original type in the call chain (T - > t) | Convert to other types in call chain (T - > R) | Call chain start (consider using) | Apply conditional statement in call chain (T - > t) | |
---|---|---|---|---|
Multi write operation | T.apply { ... } | T.run{ ... } | with(T) { ... } | T.takeIf/T.takeUnless |
Multi read operation | T.also { ... } | T.let{ ... } | - | - |
- [mandatory] for variables with high initialization cost, lazy initialization is used; if the initialization cost is not high, avoid using lazy initialization
[note] abusing lazy will create additional objects and increase performance overhead
// Counter example: the cost of creating an empty list object is not high. Do not use lazy val list by lazy { emptyList<Any>() }
- [force] by default, the lazy agent is thread safe. In a single thread environment, LazyThreadSafeMode.NONE is used to improve performance
val lazyString: String by lazy(LazyThreadSafetyMode.NONE) { }
- [mandatory] when the basic type array needs to be declared, the native type array is preferred instead of the generic array
[note] adding a basic type to a generic array will cause additional packing overhead
fun function(array: IntArray) { } // Positive example fun function(array: Array<Int>) { } // Counterexample
- [force] only literal / array constructors are allowed for vararg parameters, and references to other varargs or arrays are not allowed.
[note] passing other vararg or array references will cause additional array copies and affect performance
//definition fun multiString(vararg arg: String){...} //bad: call with array fun stringArray(array: Array<String>){ // array will has a extra copy multiString(*array) } //bad: call with vararg fun multiStringAnother(vararg arg: String){ // arg will has a extra copy multiString(*arg) } multiString("1", "2", "3") // good: call with literal multiString(*arrayOf("1", "2", "3")) // good: call with creation multiString(*Array(3) { it.toString() }) // good: call with creation
Five categories and objects
-
[mandatory] for non private classes, methods and properties that are not exposed to external modules, internal modifiers need to be added
-
Force: do not use the inner modifier for an internal class unless the internal class needs to access the external class members
-
In the subclass of forced open, the override member is open by default. If it is determined that it should not be inherited in the future, the final modifier must be added
open class Foo() : Bar() { final override fun test() { // No subclass override required, final added super.test() }
- [mandatory] var attribute is not allowed in data class. Use the copy method of the data class to make property changes.
// Counter example: data class should use val attribute data class MutableDataClass(var i: Int) { var s: String? = null }
- Force: constant is defined in the associated object. const modifier is required. The same principle in object declaration (singleton class)
// Positive example class MainFragment: Fragment() { companion object { const val TYPE_VIEW_HEADER = 0 const val TYPE_VIEW_FOOTER = 1 } } // Positive example object UserRepo { const val USER_TYPE_ADMIN = "USER_TYPE_ADMIN" }
- Force: use the object declaration instead of the class with only company object (there are no other class members in this class or the parent class)
// Counter example: class MyClass{ companion object{ fun doSth(){...} } } // Example: object MyClass{ fun doSth(){...} }
Six functions and lambda expression
- [recommended] when calling multi parameter functions, try to use named parameters to improve the readability of the code
//declaration fun reformat(str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ') { /*...*/ } // Example: reformat(str, normalizeCase = true, upperCaseFirstLetter = true, divideByCamelHumps = false, wordSeparator = '_' ) // Counter example: reformat(str, true, true, false, '_')
- [recommended] the top-level extension function is preferred to replace the static tool class
// Counter example: static method of tool class Collections object Collections { fun <T> sort(list: List<T>, c: Comparator<in T>) : List { ... } } // Positive example: top level extension function fun <T> List<T>.sort(c: Comparator<in T>) : List<T> { ... }
- [recommended] function types that need to be used multiple times in the code. You can define a type alias. Note adding names to parameters
typealias MouseClickHandler = (payload: Any, event: MouseEvent) -> Unit
- [recommended] when declaring function types, do not omit variable names, which is conducive to code completion of IDE at the call point
// Example: fun performRequest ( url: String, callback: (code: Int, cotentL String) -> Unit ) { ... } // Counter example: fun performRequest ( url: String, callback: (Int, String) -> Unit ) { ... }
- [recommended] in the block of a lambda expression, if you mainly write to an instance, the instance is declared as a Receiver; if you mainly read, the instance is declared as a parameter.
inline fun <T> T.apply(block: T.() -> Unit): T//Write to T, use apply first tvName.apply { text = "Jacky" textSize = 20f } inline fun <T> T.also(block: (T) -> Unit): T //Read T and use also first user.also { tvName.text = it.name tvAge.text = it.age }
- When the mandatory parameter contains a lambda and the method body is simple enough, use the inline keyword to modify the method
[note] the inline function will be "copied" to the calling function during compilation, which may increase the package volume. Therefore, when there are many calls to the function, inline is not recommended. When there is no lambda in the parameter, if there are too many calls and the method body is simple, inline can also be considered as appropriate
inline fun <T> complicatedProcessCalledEveryWhere(block: (T)-> Unit){...} // Counterexample fun verySimpleProcess(block: () -> Unit){...} // Counterexample
- Force: use inline function to pay attention to non local return
// Counter example: since any is inline, return will return the doSth method as a whole fun doSth(input: List<String>): Boolean { input.any { return if (it.isNotEmpty()) true else true } return false } // Positive example: add @ any tag and return any block fun doSth(input: List<String>): Boolean { input.any { return@any if (it.isNotEmpty()) true else true } return false }
Seven collective processing
- [force] give priority to using factory method instead of constructor to build List, Map and other instances
[note] avoid directly creating ArrayList and LinkedHashMap, but use mutableListOf(), mutableMapOf()
// Counter example: val list: List<String> = ArrayList<String>().apply { ... } val mutableList: MutableList<String> = ArrayList() val map: Map<String, String> = HashMap<String, String>.apply { ... } // Example: val list: List<String> = listOf("1", "2", "3") val list: List<String> = List(3) { it.toString() } val mutableList: MutableList<String> = mutableListOf() val map: Map<String, String> = mapOf("1" to "1", "2" to "2")
- [force] give priority to List instead of MutableList, and create new List instead of modifying the original MutableList to ensure the invariance of the Set. (same as Set and Map)
[exception] note that the toMutableList will create a new object. When you need to modify the original object, use the MutableList
// Counter example: val list = mutableListOf(1, 2, 3) ... list[1] = 4 //origin list modified ... // Example: val list = listOf(1, 2, 3) ... val newList = list.toMutableList() // new list created newList[1] = 4 // new list modified ...
- [force] use operator overload to replace the original set/get operation of the set, and use in to replace contents
// Example: list[0] list[0] = 1 if(0 in list) // Counter example: list.get(0) list.set(0, 1) if(list.contains(0))
VIII. Cooperation specification
- [force] to use CoroutineScope, you must cancel it at the right time, for example, in onDestroy of Activity
[note] GlobalScope can only be used in the same class as the Application life cycle.
// Counter example: class MyActivity: AppCompactActivity(){ override fun onCreate(...){ GlobalScope.launch{...} } } // Example: class MyActivity: AppCompactActivity(), CoroutineScope by MainScope() { //In the actual project, it is recommended that the base class Activity realize CoroutineScope and cancel in onDestroy //All the processes created in the sub Activity become its sub processes, which can be recycled automatically override fun onCreate(...){ launch{...} } override fun onDestroy(){ cancel() } }
- [mandatory] only methods that call other suspend functions are allowed to add the suspend modifier
// Counter example: the following suspend modifier has no meaning suspend fun parseFile(path: String) = File(path).bufferedReader().readText()
- [force] suspend method (or other time-consuming method) that doesn't care about the return value. Use launch instead of async to start
GlobalScope.async{ doSthInBackground() } // Counterexample GlobalScope.launch{ doSthInBackground() } // Positive example
- [force] async is used to synchronize the results of multiple asynchronous calls. When a single asynchronous call (equivalent to only switching threads), use withContext
// Example: launch { val data = withContext(Dispatchers.Default) { /* code */ } ... } // Counter example: launch { val data = async(Dispatchers.Default) { /* code */ }.await() ... }
IX Java compatibility
- [recommended] add @ nullable / @ notnull (or @ NonNull) annotation for parameter / return / generic types in Java for the convenience of Kotlin type system interpretation
@NotNull Set<@NotNull String> toSet(@NotNull Collection<@NotNull String> elements) { ... }
- When the Java object's nullability can be determined by force, the reference variables of the object are declared with clear types to avoid using platform types with unknown nullability
val s: String? = person.name // Positive example val s: String = person.name // Positive example val s = person.name // Counterexample: platform type String! Nullability unknown
- [recommended] when you need to expose the attributes of Kotlin (common member attributes, associated object attributes, object declaration attributes) to Java, add @ JvmStatic annotation to the attributes, which can be accessed in Java in the way of attributes rather than getter s. Be careful not to add @ JvmStatic to constant properties, which will generate static methods instead of static variables
// Counter example: class Key(val value: Int) { companion object { const val INTEGER_ONE = 1 val BIG_INTEGER_ONE = BigInteger.ONE } } object Util { @JvmStatic val BIG_INTEGER_TEN = BigInteger.TEN } // in java Key key = new Key(5); System.out.println(key.getValue());//Call getter System.out.println(Key.INTEGER_ONE);//Accessing static constants System.out.println(Key.Companion.getBIG_INTEGER_ONE());//Calling getter through companion object System.out.println(Util.getBIG_INTEGER_TEN());//By static method call // Example: class Key(@JvmField val value: Int) { companion object { const val INTEGER_ONE = 1 @JvmField val BIG_INTEGER_ONE = BigInteger.ONE } } object Util { @JvmField val BIG_INTEGER_TEN = BigInteger.TEN } //in java Key key = new Key(5); System.out.println(key.value);//Access field System.out.println(Key.INTEGER_ONE);//Accessing static constants System.out.println(Key.BIG_INTEGER_ONE);//Accessing static fields System.out.println(Util.BIG_INTEGER_TEN);//Accessing static fields
- [recommended] when you need to expose Kotlin's functions (associated objects, functions declared by objects) to Java, add @ JvmStatic annotation to the functions
// Counter example: class Key(val value: Int) { companion object { fun doWork() {} } } object Util { fun doWork(){} } //in java Key.Companion.doWork();//Calling methods through companion objects Util.INSTANCE.doWork();//Call through instance // Example: class Key(val value: Int) { companion object { @JvmStatic fun doWork() {} } } object Util { @JvmStatic fun doWork(){} } //in java Key.doWork();//Call static method Util.doWork();//Call static method
- [recommended] for functions with default parameters, add @ JvmOverloads annotation
// Counter example: class Sample { fun multiParam(param1: Int = 1, param2: String = "2", param3: Double = 3.0) { } } //in java Sample().multiParam(1, "2", 3D);//All 3 parameters must be specified // Example: class Sample { @JvmOverloads fun multiParam(param1: Int = 1, param2: String = "2", param3: Double = 3.0) { } } //in java, all overloads can be used Sample().multiParam(); Sample().multiParam(1); Sample().multiParam(1, "2"); Sample().multiParam(1, "2", 3D);
- [recommended] methods that may throw exceptions need to be exposed to Java calls with @ Throws annotation
[note] Java will treat @ throws as a throws declaration and force try catch
//kotlin @Throws(IOException::class) fun readFile(file: String) : OutputStream = {...} //java caller try { //try...catch is mandatory i = readFile("./config.xml") } catch (IOException e) { e.printStackTrace(); }
- [mandatory] when calling Java's "register / unregister" SAM, you need to ensure the uniqueness of the incoming instance, and you are not allowed to create an instance object through a lambda, otherwise you cannot guarantee that the same instance is passed.
//defined in java public class Widget { public interface Listener { void onEvent(@NotNull Widget widget); } public int getListenerCount() { ... } public void addListener(@NotNull Listener listener) { ... } public void removeListener(@NotNull Listener listener) { ... } } // Counter example 1: anonymous inner class, each time a new instance is created val widget = Widget() widget.addListener{ widget: Widget -> println("Listened to $widget") } println(widget.listenerCount) // will print 1 ... widget.removeListener{ widget: Widget -> println("Listened to $widget") } println(widget.listenerCount) // still print 1 // Counter example 2: using lambda directly, each time it is packaged into a new instance val widget = Widget() //listener type is Function1 when using lambda expression val listener = { widget: Widget -> println("Listened to $widget") } widget.addListener(listener) //listener will be wrapped into Listener type println(widget.listenerCount) // will print 1 ... widget.removeListener(listener) // listener will be wrapped into another instance println(widget.listenerCount) // still print 1 // Positive example: use SAM constructor, or directly use object expression, the instance is the same val widget = Widget() //listener type is Widget.Listener when using SAM constructor val listener = Widget.Listener { widget: Widget -> println("Listened to $widget") } widget.addListener(listener) //no wrapping occurs println(widget.listenerCount) // will print 1 ... widget.removeListener(listener) // no wrapping occurs println(widget.listenerCount) // still print 0
X Android development specifications
- [recommended] activate Activiy to recommend implementing static methods through associated objects to reduce key exposure
class MyActivity{ companion object{ private const val USER_NAME: String = "user_name" @JvmStatic fun startActivity(userName: String){...} } }
- [recommended] use @ Parcelize annotation to simplify the Parcelable class
[note] it is not necessary to modify the read/write method related to Parcelable as a whole for adding a new attribute.
@Parcelize data class User(val id: String, val name: String, ...): Parcelable
- [mandatory] in Activity and Fragment, findViewById is not used to get the sub View of the layout. Instead, kotlin Android extension plug-in is used. Hump rather than underline is recommended for id naming of View