Android - How To Improve Code Quality
Android - How To Improve Code Quality
quality
The style guide for Kotlin convention
Link: https://kotlinlang.org/docs/reference/coding-conventions.html
Link: https://developer.android.com/kotlin/style-guide
Link: https://www.oracle.com/technetwork/java/javase/documentation/codeconvtoc-
136057.html
Rule of 30
If an element consists of more than 30 subelements, it is highly probable that there is a
serious problem:
a) Methods should not have more than an average of 30 code lines (not counting line spaces
and comments).
b) A class should contain an average of less than 30 methods, resulting in up to 900 lines of
code.
c) A package shouldn’t contain more than 30 classes, thus comprising up to 27,000 code
lines.
d) Subsystems with more than 30 packages should be avoided. Such a subsystem would
count up to 900 classes with up to 810,000 lines of code.
e) A system with 30 subsystems would thus possess 27,000 classes and 24.3 million code
lines.
1
I. Kotlin convention
To configure the IntelliJ formatter according to this style guide, please install Kotlin plugin version
1.2.20 or newer, go to Settings | Editor | Code Style | Kotlin, click on "Set from…" link in the upper
right corner, and select "Predefined style / Kotlin style guide" from the menu.
a. Classes and objects start with an upper case letter and use camel humps:
b. Functions, properties and local variables start with a lower case letter and use camel humps
and no underscores:
c. Exception: factory functions used to create instances of classes can have the same name as
the class being created:
d. Test methods
class MyTestCase {
@Test fun `ensure everything works`() { ... }
e. Constants, Enum
f. Names of top-level or object properties which hold objects with behavior or mutable data
h. If a class has two properties which are conceptually the same but one is part of a public API and
another is an implementation detail, use an underscore as the prefix for the name of the private
property:
2
class C {
private val _elementList = mutableListOf<Element>()
2. Formatting (https://kotlinlang.org/docs/reference/coding-conventions.html#formatting)
3. Copyright / License
If a copyright or license header belongs in the file it should be placed at the immediate top in a multi-
line comment.
/*
* Copyright 2017 Google, Inc.
*
* ...
*/
Do not use a KDoc-style or single-line-style comment.
/**
* Copyright 2017 Google, Inc.
*
* ...
*/
4. Expressions
try {
doSomething()
} catch (e: Exception) {} // WRONG!
try {
doSomething()
} catch (e: Exception) {
e.printStackTrace();
} // Okay
3
5. Horizontal whitespace
// WRONG!
for(i in 0..1) {
}
// Okay
for (i in 0..1) {
}
// WRONG!
}else {
}
// Okay
} else {
}
// WRONG!
if (list.isEmpty()){
}
// Okay
if (list.isEmpty()) {
}
// Okay
package com.example.deepspace
// WRONG!
package com.example.deepSpace
// WRONG!
package com.example.deep_space
6. Braces
if (string.isEmpty())
return // WRONG!
if (string.isEmpty()) {
return // Okay
}
// Don't
fun getDefaultLocale(deliveryArea: String): Locale {
val deliverAreaLower = deliveryArea.toLowerCase()
if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
return Locale.GERMAN
}
4
if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
return Locale.ENGLISH
}
if (deliverAreaLower == "france") {
return Locale.FRENCH
}
return Locale.ENGLISH
}
// Do
fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
"germany", "austria" -> Locale.GERMAN
"usa", "great britain" -> Locale.ENGLISH
"france" -> Locale.FRENCH
else -> Locale.ENGLISH
}
//Don't
object StringUtil {
fun countAmountOfX(string: String): Int{
return string.length - string.replace("x", "").length
}
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")
//Do
fun String.countAmountOfX(): Int {
return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()
//Don't
val config = SearchConfig()
.setRoot("~/folder")
.setTerm("game of thrones")
.setRecursive(true)
.setFollowSymlinks(true)
//Do
val config2 = SearchConfig2(
root = "~/folder",
term = "game of thrones",
recursive = true,
followSymlinks = true
)
5
4. apply() for Grouping Object Initialization
//Don't
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4
//Do
val dataSource = BasicDataSource().apply {
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://domain:3309/db"
username = "username"
password = "password"
maxTotal = 40
maxIdle = 40
minIdle = 4
}
//Don't
fun find(name: String){
find(name, true)
}
fun find(name: String, recursive: Boolean){
}
//Do
fun find(name: String, recursive: Boolean = true){
}
//Don't
if (order == null || order.customer == null || order.customer.address == null){
throw IllegalArgumentException("Invalid Order")
}
val city = order.customer.address.city
//Do
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")
6
b. Avoid if-type Checks
//Don't
if (service !is CustomerService) {
throw IllegalArgumentException("No CustomerService")
}
service.getCustomer()
//Do
service as? CustomerService ?: throw IllegalArgumentException("No CustomerService")
service.getCustomer()
//Don't
order!!.customer!!.address!!.city
“You may notice that the double exclamation mark looks a bit rude: it’s almost like you’re yelling at the
compiler. This is intentional. The designers of Kotlin are trying to nudge you toward a better solution
that doesn’t involve making assertions that can’t be verified by the compiler.” Kotlin in Action by
Dmitry Jemerov and Svetlana Isakova
// Don't
fun send(target: String){}
// Do
fun send(target: EmailAddress){}
// expressive, readable, type-safe
// Don't
fun mapToDTO(entity: SnippetEntity): SnippetDTO {
val dto = SnippetDTO(
code = entity.code,
date = entity.date,
author = "${entity.author.firstName} ${entity.author.lastName}"
)
return dto
}
7
// Do
fun mapToDTO(entity: SnippetEntity) = SnippetDTO(
code = entity.code,
date = entity.date,
author = "${entity.author.firstName} ${entity.author.lastName}"
)
val dto = mapToDTO(entity)
// Do
fun SnippetEntity.toDTO() = SnippetDTO(
code = code,
date = date,
author = "${author.firstName} ${author.lastName}"
)
val dto = entity.toDTO()
// Don't
class UsersClient(baseUrl: String, appName: String) {
private val usersUrl: String
private val httpClient: HttpClient
init {
usersUrl = "$baseUrl/users"
val builder = HttpClientBuilder.create()
builder.setUserAgent(appName)
builder.setConnectionTimeToLive(10, TimeUnit.SECONDS)
httpClient = builder.build()
}
fun getUsers(){
//call service using httpClient and usersUrl
}
}
// Do
class UsersClient(baseUrl: String, appName: String) {
private val usersUrl = "$baseUrl/users"
private val httpClient = HttpClientBuilder.create().apply {
setUserAgent(appName)
setConnectionTimeToLive(10, TimeUnit.SECONDS)
}.build()
fun getUsers(){
//call service using httpClient and usersUrl
}
}
8
10. object for Stateless Interface Implementations
//Do
object StringToInstantConverter : Converter<String, Instant> {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z")
.withLocale(Locale.UK)
.withZone(ZoneOffset.UTC)
11. Destructuring
//Do
data class ServiceConfig(val host: String, val port: Int)
fun createServiceConfig(): ServiceConfig {
return ServiceConfig("api.domain.io", 9389)
}
//destructuring in action:
val (host, port) = createServiceConfig()
//Do
val map = mapOf("api.domain.io" to 9389, "localhost" to 8080)
for ((host, port) in map){
//...
}
listOf, mapOf and the infix function to can be used to create structs (like JSON) quite concisely
//Do
val customer = mapOf(
"name" to "Clair Grube",
"age" to 30,
"languages" to listOf("german", "english"),
"address" to mapOf(
"city" to "Leipzig",
"street" to "Karl-Liebknecht-Straße 1",
"zipCode" to "04107"
)
)
9
13. Kotlin Null Safety
https://www.callicoder.com/kotlin-nullable-types-null-safety/
The !! Operator can cause kotlin.KotlinNullPointerException. So, we should use other operators to
replace it.
https://viblo.asia/p/loai-bo-toan-tu-null-checks-trong-code-kotlin-cua-ban-V3m5WAyvZO7
https://phauer.com/2019/sealed-classes-exceptions-kotlin/
// Definition
sealed class UserProfileResult {
data class Success(val userProfile: UserProfileDTO) : UserProfileResult()
data class Error(val message: String, val cause: Exception? = null) : UserProfileResult()
}
// Usage
val avatarUrl = when (val result = client.requestUserProfile(userId)) {
is UserProfileResult.Success -> result.userProfile.avatarUrl
is UserProfileResult.Error -> "http://domain.com/defaultAvatar.png"
//The solution for the more complicated example could look like this:
10
}
https://www.journaldev.com/19467/kotlin-let-run-also-apply-with
https://proandroiddev.com/the-difference-between-kotlins-functions-let-apply-with-run-and-else-
ca51a4c696b8
a. let
fun main(
fun main(args: Array<String>) {
var str = "Hello World"
str.let { println("$it!!") }
println(str)
}
//Prints
//Hello World!!
//Hello World
var a = 1
var b= 2
println(a)
//5
b. Nesting let
var x = "Anupam"
x = x.let { outer ->
outer.let { inner ->
println("Inner is $inner and outer is $outer")
"Kotlin Tutorials Inner let"
}
"Kotlin Tutorials Outer let" // Block return value
}
println(x)
//Prints
//Inner is Anupam and outer is Anupam
//Kotlin Tutorials Outer let
11
var name : String? = "Kotlin let null check"
name?.let { println(it) } //prints: Kotlin let null check
name = null
name?.let { println(it) } //nothing happens
d. run
i. Similar to the let function, the run function also returns the last statement.
ii. Unlike let, the run function doesn’t support the it keyword.
//Prints
//This is Kotlin Tutorial
//This is run function
println(p)
//Prints
//p was null. Setting default value to:
//Kotlin
e. also
i. Unlike let, it returns the original object instead of any new return data
var m = 1
m = m.also { it + 1 }.also { it + 1 }
println(m) //prints 1
println(l)
println(al)
println(person)
//Prints
//kotlin.Unit
//Person(name=Anupam, tutorial= Swift)
//Person(name=Anupam, tutorial= Swift)
12
f. Apply
//Person(name=Anupma, tutorial=Swift)
g. With
i. Like apply, with is used to change instance properties without the need to call dot operator
over the reference every time.
with(person)
{
name = "No Name"
tutorial = "Kotlin tutorials"
}
println(person) //Person(name=No Name, tutorial=Kotlin tutorials)
Apply, run, with: if we want to access to clas from inside of function, we can use this@class
13
Extension function is better than normal function because it reduces cheking null
https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties-and-variables
Normally, properties declared as having a non-null type must be initialized in the constructor.
However, fairly often this is not convenient. For example, properties can be initialized through
dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null
initializer in the constructor, but you still want to avoid null checks when referencing the property
inside the body of a class.
To handle this case, you can mark the property with the lateinit modifier:
class MyTest {
lateinit var subject: TestSubject
Accessing a lateinit property before it has been initialized throws a special exception
fun main() {
User().showCompany()
}
class User() {
lateinit var company: Company
fun showCompany() {
println(company.toString())
}
}
class Company
From Kotlin 1.2, To check whether a lateinit var has already been initialized, use .isInitialized
fun showCompany2() {
println(if (::company.isInitialized) {
company.toString()
} else {
"not Initialized"
14
})
}
=> Solution:
- Only use lateinit var for properties which can be initialized through dependency injection, or in the
setup method of a unit test
class Person(val firstName: String, val lastName: String, var age: Int) { ... }
i. Secondary Constructors
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
If the class has a primary constructor, each secondary constructor needs to delegate to the
primary constructor
If you do not want your class to have a public constructor, you need to declare an empty
primary constructor with non-default visibility:
15
class Derived() : Base() {
override fun v() { ... }
}
open class A {
open fun f() { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } // interface members are 'open' by default
fun b() { print("b") }
}
https://proandroiddev.com/kotlin-data-classes-enough-boilerplate-c4647e475485
https://www.callicoder.com/kotlin-data-classes/
https://kotlinlang.org/docs/reference/data-classes.html
Kotlin has a better solution for classes that are used to hold data/state. It’s called a Data Class. A
Data Class is like a regular class but with some additional functionalities.
The compiler automatically derives the following members from all properties declared in the primary
constructor: equals()/hashCode(), toString(), copy(), componentN()
i. Equals, toString()
customer1.age = 10
customer2.age = 20
println(customer1)
16
println(customer2..toString())
//Prints
com.unitestexample.myapplication.ExampleUnitTest$Customer@61e717c2
com.unitestexample.myapplication.ExampleUnitTest$Customer@66cd51c3
Case 1: false
Case 2: false
ii. Copy()
iii. componentN()
println(customer1.component1()) //1
println(customer1.component2()) //John
https://www.baeldung.com/kotlin-lambda-expressions
https://www.journaldev.com/18835/kotlin-lambda-higher-order-functions
https://blog.egorand.me/where-do-i-put-my-constants-in-kotlin/
https://viblo.asia/p/nhung-dieu-nen-biet-khi-code-kotlin-cho-android-p1-bWrZneBwKxw
https://viblo.asia/p/chung-ta-nen-dinh-nghia-constants-o-dau-trong-kotlin-oOVlY1jol8W
https://www.journaldev.com/20567/kotlin-interview-questions
The source file name consists of the case-sensitive name of the top-level class it contains
plus the .java extension.
17
b. File encoding: UTF-8
c. Whitespace characters: Aside from the line terminator sequence, the ASCII horizontal space
character (0x20) is the only whitespace character that appears anywhere in a source file. This
implies that:
- All other whitespace characters in string and character literals are escaped.
- Tab characters are not used for indentation.
d. Special escape sequences: For any character that has a special escape sequence (\b, \t, \n,
\f, \r, \", \' and \\), that sequence is used rather than the corresponding octal (e.g. \012) or
Unicode (e.g. \u000a) escape.
e. Non-ASCII characters:
2. Source file structure
A source file consists of, in order:
- License or copyright information, if present
- Package statement
- Import statements
- Exactly one top-level class
Exactly one blank line separates each section that is present.
3. Formatting
a. Braces: Braces are used where optional Braces, used with if, else, for, do and while
statements, even when the body is empty or contains only a single statement.
b. Nonempty blocks: K & R style
Braces follow the Kernighan and Ritchie style ("Egyptian brackets") for nonempty blocks and
block-like constructs:
- No line break before the opening brace.
- Line break after the opening brace.
- Line break before the closing brace.
- Line break after the closing brace, only if that brace terminates a statement or terminates the
body of a method, constructor, or named class. For example, there is no line break after the
brace if it is followed by else or a comma.
c. Whitespace:
Vertical Whitespace
- A single blank line may also appear anywhere it improves readability, for example
between statements to organize the code into logical subsections. A blank line before
the first member or initializer, or after the last member or initializer of the class, is
neither encouraged nor discouraged.
- Multiple consecutive blank lines are permitted, but never required (or encouraged).
Horizontal whitespace
✓ Separating any reserved word, such as if, for or catch, from an open parenthesis (()
that follows it on that line
✓ Separating any reserved word, such as else or catch, from a closing curly brace (})
that precedes it on that line
✓ Before any open curly brace ({), with two exceptions:
18
○ @SomeAnnotation({a, b}) (no space is used)
○ String[][] x = {{"foo"}}; (no space is required between {{, by item 8
below)
✓ On both sides of any binary or ternary operator. This also applies to the following
"operator-like" symbols:
○ the ampersand in a conjunctive type bound: <T extends Foo & Bar>
○ the pipe for a catch block that handles multiple exceptions: catch
(FooException | BarException e)
○ the colon (:) in an enhanced for ("foreach") statement
○ the arrow in a lambda expression: (String str) -> str.length()
✓ but not
○ the two colons (::) of a method reference, which is written like
Object::toString
○ the dot separator (.), which is written like object.toString()
✓ After ,:; or the closing parenthesis ()) of a cast
✓ On both sides of the double slash (//) that begins an end-of-line comment. Here, multiple
spaces are allowed, but not required.
✓ Between the type and variable of a declaration: List<String> list
✓ Optional just inside both braces of an array initializer
○ new int[] {5, 6} and new int[] { 5, 6 } are both valid
✓ Between a type annotation and [] or ....
This rule is never interpreted as requiring or forbidding additional space at the start or end of a line; it
addresses only interior space.
19