# Code Style and Conventions Standards for Kotlin
This document outlines the coding style and conventions standards for Kotlin, focusing on formatting, naming, and stylistic consistency to improve code readability, maintainability, and overall quality. It adopts a modern approach aligned with the latest Kotlin features and ecosystem practices.
## 1. Formatting
Consistent code formatting is crucial for readability. These standards ensure a unified look and feel across all Kotlin projects.
### 1.1. Indentation
* **Do This:** Use 4 spaces for indentation. Avoid tabs.
* **Don't Do This:** Use tabs or inconsistent spacing.
* **Why:** Consistent spacing enhances readability regardless of the editor used.
"""kotlin
// Correct:
fun calculateArea(width: Int, height: Int): Int {
val area = width * height
return area
}
// Incorrect:
fun calculateArea(width: Int, height: Int): Int {
val area = width * height
return area
}
"""
### 1.2. Line Length
* **Do This:** Limit lines to a maximum of 120 characters.
* **Don't Do This:** Exceed the line length limit unless unavoidable (URLs, generated code).
* **Why:** Shorter lines are easier to read on various screen sizes and encourage better code organization.
"""kotlin
// Correct:
fun processData(data: List, transform: (String) -> Int, filter: (Int) -> Boolean): List {
return data.map(transform).filter(filter)
}
// Incorrect:
fun processData(data: List, transform: (String) -> Int, filter: (Int) -> Boolean): List { return data.map(transform).filter(filter) }
"""
### 1.3. Whitespace
* **Do This:** Use spaces around operators, after commas, and between keywords and parentheses.
* **Don't Do This:** Omit spaces around operators or use multiple spaces.
* **Why:** Improves readability and reduces visual clutter.
"""kotlin
// Correct:
val x = a + b
val list = listOf(1, 2, 3)
// Incorrect:
val x=a+b
val list = listOf(1,2,3)
"""
### 1.4. Blank Lines
* **Do This:** Use blank lines to separate logical sections of code, such as between functions, classes, and blocks of code.
* **Don't Do This:** Omit blank lines or use excessive blank lines.
* **Why:** Improves readability by visually separating distinct code segments.
"""kotlin
// Correct:
class DataProcessor {
fun process(data: String): Result {
// processing logic
}
fun validate(data: String): Boolean {
// validation logic
}
}
// Incorrect:
class DataProcessor {
fun process(data: String): Result {
// processing logic
}
fun validate(data: String): Boolean {
// validation logic
}
}
"""
### 1.5. File Structure
* **Do This:** If a Kotlin file contains a single class, name the file the same as the class. If it contains multiple top-level declarations or classes, use a descriptive name in PascalCase describing functionality.
* **Don't Do This:** Use arbitrary or nondescript filenames.
* **Why:** Consistent file naming improves project navigation and organization.
"""
// Correct:
// File: UserProfile.kt
class UserProfile {
// ...
}
// File: DataProcessingUtils.kt
fun processData() { /* ... */ }
"""
## 2. Naming Conventions
Consistent naming conventions help in understanding the purpose and scope of variables, functions, classes, and other constructs.
### 2.1. Classes and Interfaces
* **Do This:** Use PascalCase for class and interface names.
* **Don't Do This:** Use snake_case or camelCase for class names.
* **Why:** PascalCase is a widely accepted convention for classes and interfaces in many languages including Java.
"""kotlin
// Correct:
class UserProfile
interface DataService
// Incorrect:
class userProfile
interface dataService
"""
### 2.2. Functions
* **Do This:** Use camelCase for function names.
* **Don't Do This:** Use PascalCase or snake_case for function names.
* **Why:** camelCase is commonly used for functions, making them easily distinguishable from classes.
"""kotlin
// Correct:
fun calculateTotal()
fun getUserName()
// Incorrect:
fun CalculateTotal()
fun get_user_name()
"""
### 2.3. Variables
* **Do This:** Use camelCase for variable names.
* **Don't Do This:** Use PascalCase or snake_case for variable names.
* **Why:** camelCase for variables maintains consistency with function names.
"""kotlin
// Correct:
val userName = "John Doe"
val itemCount = 10
// Incorrect:
val UserName = "John Doe"
val item_count = 10
"""
### 2.4. Constants
* **Do This:** Use UPPER_SNAKE_CASE for constants.
* **Don't Do This:** Use camelCase or PascalCase for constants.
* **Why:** UPPER_SNAKE_CASE clearly indicates that the variable is a constant. Declare using "const val" where possible for compile-time constants, or "val" for runtime constants.
"""kotlin
// Correct:
const val MAX_USERS = 100
val API_KEY = "abcdef123456"
// Incorrect:
val maxUsers = 100
val apiKey = "abcdef123456"
"""
### 2.5. Package Names
* **Do This:** Use lowercase and avoid underscores. Use reverse domain name notation.
* **Don't Do This:** Use uppercase letters or underscores.
* **Why:** Consistent package naming prevents naming conflicts and follows Java conventions.
"""kotlin
// Correct:
package com.example.myapp.data
// Incorrect:
package com.Example.MyApp.Data
package com_example_myapp_data
"""
### 2.6. Enum Classes
* **Do This:** Use PascalCase for enum class names and UPPER_SNAKE_CASE for enum constants.
* **Don't Do This:** Mix naming conventions or use inconsistent casing.
* **Why:** Ensures uniformity and easy identification of enum elements.
"""kotlin
// Correct:
enum class UserStatus {
ACTIVE,
INACTIVE,
PENDING
}
// Incorrect:
enum class UserStatus {
Active,
inactive,
Pending
}
"""
## 3. Stylistic Consistency
Maintaining stylistic consistency makes code easier to read and understand.
### 3.1. Immutability
* **Do This:** Prefer "val" (immutable) over "var" (mutable) whenever possible.
* **Don't Do This:** Use "var" unnecessarily.
* **Why:** Immutability reduces the risk of bugs and makes code easier to reason about.
"""kotlin
// Correct:
val name = "John" // Cannot be changed
var age = 30 // Can be changed
// Incorrect:
var name = "John" // Unnecessarily mutable
"""
### 3.2. Null Safety
* **Do This:** Leverage Kotlin's null safety features ("?", "!!", "?:") to handle nullable values safely. Avoid unnecessary null checks. Use "let", "run", "with", "apply", and "also" appropriately for null handling.
* **Don't Do This:** Use "!!" without understanding the risks. Avoid redundant null checks.
* **Why:** Prevents NullPointerExceptions and improves code reliability.
"""kotlin
// Correct:
val name: String? = getName()
val length = name?.length ?: 0 // Safe call and Elvis operator
// Incorrect:
val name: String? = getName()
val length = name!!.length // Potential NullPointerException
"""
### 3.3. Functions vs. Properties
* **Do This:** Use properties for simple data retrieval without side effects. Use functions for operations that involve logic or side effects.
* **Don't Do This:** Use functions for simple data retrieval or properties for complex operations.
* **Why:** Clarity in intent: properties are for data, functions are for actions.
"""kotlin
// Correct:
val fullName: String get() = "$firstName $lastName" // Property
fun calculateAge(): Int { /* ... */ } // Function
// Incorrect:
fun fullName(): String = "$firstName $lastName" // Confusing function
val age: Int get() { /* ... */ } // Misleading property
"""
### 3.4. Expression Functions
* **Do This:** Use expression functions for simple, single-expression functions.
* **Don't Do This:** Use expression functions for complex functions with multiple statements.
* **Why:** Expression functions are more concise and readable for simple operations.
"""kotlin
// Correct:
fun square(x: Int) = x * x
// Incorrect:
fun complexCalculation(x: Int): Int {
val y = x * 2
val z = y + 1
return z
}
"""
### 3.5. Control Flow
* **Do This:** Prefer "when" over "if-else" chains when dealing with multiple conditions, particularly with sealed classes or enums.
* **Don't Do This:** Nest "if-else" statements excessively.
* **Why:** "when" is more readable and concise.
"""kotlin
// Correct:
enum class State {
IDLE,
RUNNING,
FINISHED
}
fun processState(state: State) = when (state) {
State.IDLE -> "Idling"
State.RUNNING -> "Running"
State.FINISHED -> "Finished"
}
// Incorrect:
fun processState(state: State) {
if (state == State.IDLE) {
println("Idling")
} else if (state == State.RUNNING) {
println("Running")
} else if (state == State.FINISHED) {
println("Finished")
}
}
"""
### 3.6. Extension Functions
* **Do This:** Use extension functions to add functionality to existing classes, but avoid overusing them. Group related extensions in relevant files.
* **Don't Do This:** Overuse extension functions to the point where they clutter the codebase or modify core classes in unexpected ways.
* **Why:** Extension functions provide a clean way to add functionality without modifying existing classes.
"""kotlin
// Correct:
fun String.removeWhitespace(): String = this.replace("\\s+".toRegex(), "")
// Using the extension
val text = " Hello World "
val cleanText = text.removeWhitespace()
"""
### 3.7. Lambdas and Higher-Order Functions
* **Do This:** Use lambdas and higher-order functions to promote code reuse and flexibility. Keep lambdas concise, ideally within 2-3 lines. For longer lambdas, consider extracting the logic into a separate function.
* **Don't Do This:** Overuse lambdas to the point where they make code difficult to understand.
* **Why:** Lambdas and higher-order functions enable powerful abstractions.
"""kotlin
// Correct:
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
// If the lambda is long, extract it.
val isEligible = { age: Int ->
age >= 18 && age <= 65
}
val ages = listOf(16, 25, 70, 30)
val eligibleAges = ages.filter(isEligible)
"""
### 3.8. Data Classes
* **Do This:** Use data classes to represent data-heavy classes that primarily hold data.
* **Don't Do This:** Use regular classes for data-centric entities.
* **Why:** Data classes automatically generate useful methods like "equals()", "hashCode()", "toString()", and "copy()".
"""kotlin
// Correct:
data class User(val id: Int, val name: String, val email: String)
//Using a data class
val user1 = User(1, "John", "john@example.com")
val user2 = user1.copy(name = "Jane")
"""
### 3.9. Sealed Classes
* **Do This:** Use sealed classes to represent a restricted class hierarchy, providing exhaustive "when" statements.
* **Don't Do This:** Use regular classes or interfaces when a restricted hierarchy is needed.
* **Why:** Sealed classes enhance type safety and exhaustiveness checks.
"""kotlin
// Correct:
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
}
fun processResult(result: Result) = when (result) {
is Result.Success -> "Success: ${result.data}"
is Result.Error -> "Error: ${result.message}"
}
//The compiler forces you to handle all cases
"""
### 3.10. Annotations
* **Do This:** Follow conventions for annotation placement. Place class-level annotations before the class definition; field-level annotations immediately before the field. Use "@file:" annotations for file-level configurations.
* **Don't Do This:** Inconsistently place annotations or omit required annotations (e.g., "@Override").
* **Why:** Consistent annotation placement improves readability and clarifies metadata relationships.
"""kotlin
// Correct:
@Serializable
data class User(
@SerialName("user_id")
val id: Int,
val name: String
)
@file:JvmName("Utilities")
package com.example
fun utilityFunction() { ... }
"""
## 4. Modern Kotlin Features and Best Practices
Employing modern Kotlin features improves efficiency, readability, and maintainability.
### 4.1. Coroutines
* **Do This:** Use coroutines for asynchronous programming to simplify concurrent code. Leverage structured concurrency using "CoroutineScope" and "viewModelScope".
* **Don't Do This:** Use traditional threads directly.
* **Why:** Coroutines are lightweight and provide a more structured way to handle concurrency.
"""kotlin
// Correct:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("World!")
}
println("Hello,")
job.join()
}
//Using structured concurrency
class MyViewModel : ViewModel() {
private val viewModelScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun fetchData() {
viewModelScope.launch {
// Asynchronous data fetching
}
}
override fun onCleared() {
super.onCleared()
viewModelScope.cancel() // Cancel all coroutines when ViewModel is destroyed
}
}
"""
### 4.2. Collection Operations
* **Do This:** Use Kotlin’s collection operations ("map", "filter", "reduce", etc.) for data transformations and filtering.
* **Don't Do This:** Use traditional loops for simple collection operations.
* **Why:** Collection operations are more concise and expressive.
"""kotlin
// Correct:
val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }
// Incorrect:
val squaredNumbers = mutableListOf()
for (number in numbers) {
squaredNumbers.add(number * number)
}
"""
### 4.3. Inline Classes and Value Classes
* **Do This:** Use inline classes (value classes in older Kotlin versions) to wrap primitive types, providing type safety without runtime overhead.
* **Don't Do This:** Use primitive types directly when type safety is important.
* **Why:** Improves performance and type safety.
"""kotlin
// Correct:
@JvmInline
value class UserId(val id: Long)
fun findUser(id: UserId) { /* ... */ }
//Usage
val userId = UserId(12345)
findUser(userId)
"""
### 4.4. Delegates
* **Do This:** Employ property delegates for common property behaviors, such as lazy initialization, observable properties, and vetoable properties.
* **Don't Do This:** Reimplement common property behaviors manually.
* **Why:** Delegates promote code reuse and reduce boilerplate.
"""kotlin
// Correct:
import kotlin.properties.Delegates
class Example {
val lazyValue: String by lazy {
println("Computed!")
"Hello"
}
var observableValue: Int by Delegates.observable(0) {
property, oldValue, newValue ->
println("Property ${property.name} changed from $oldValue to $newValue")
}
}
//Usage
val example = Example()
println(example.lazyValue) // Computed! Hello
example.observableValue = 10 // Property observableValue changed from 0 to 10
"""
### 4.5. Contracts
* **Do This:** Utilize contracts to improve nullability and smart cast analysis. This allows the compiler to understand complex conditions and avoid unnecessary checks.
* **Don't Do This:** Rely on runtime checks for conditions that can be statically analyzed using contracts.
* **Why:** Contracts provide compile-time guarantees, enhancing code safety and performance.
"""kotlin
import kotlin.contracts.*
fun String?.isNullOrLengthGreaterThan5(): Boolean {
contract {
returns(true) implies (this@isNullOrLengthGreaterThan5 == null || this@isNullOrLengthGreaterThan5.length > 5)
}
return this == null || this.length > 5
}
fun processString(str: String?) {
if (str.isNullOrLengthGreaterThan5()) {
// str is smart-casted to String? because it could be null.
println("String is null or longer than 5 characters.")
} else {
// str is smart-casted to String (non-null) because the contract guarantees it.
println("String length ${str.length}")
}
}
"""
## 5. Anti-Patterns to Avoid
Recognizing and avoiding common anti-patterns improves code quality.
### 5.1. Excessive Null Checks
* **Anti-Pattern:** Performing redundant null checks when Kotlin's null safety features can handle nullability more elegantly.
* **Solution:** Use safe calls ("?."), Elvis operator ("?:"), and "let" blocks to handle nullable values.
"""kotlin
// Anti-Pattern:
val name: String? = getName()
if (name != null) {
println(name.length)
}
// Correct:
val name: String? = getName()
name?.let { println(it.length) }
"""
### 5.2. Premature Optimization
* **Anti-Pattern:** Optimizing code before identifying performance bottlenecks through profiling.
* **Solution:** Write clear and maintainable code first. Profile and optimize only when necessary.
### 5.3. Ignoring Exceptions
* **Anti-Pattern:** Catching exceptions and doing nothing, which can mask errors and lead to unexpected behavior.
* **Solution:** Log exceptions, handle them appropriately, or rethrow them.
"""kotlin
// Anti-Pattern:
try {
// ...
} catch (e: Exception) {
// Ignoring the exception
}
// Correct:
try {
// ...
} catch (e: Exception) {
println("Error: ${e.message}") // Log the error
throw e // Rethrow if cannot handle
}
"""
### 5.4. God Classes
* **Anti-Pattern:** Creating large, monolithic classes that handle multiple responsibilities.
* **Solution:** Follow the Single Responsibility Principle and decompose classes into smaller, more manageable units.
### 5.5. Feature Envy
* **Anti-Pattern:** A class excessively using the methods of another class. This often indicates that the behavior should reside in the class being envied.
* **Solution:** Move the relevant methods to the class where they belong to reduce coupling.
## 6. Tooling and Automation
Leveraging tooling automates code style enforcement and improves consistency.
### 6.1. IntelliJ IDEA / Android Studio
* **Recommendation:** Configure IntelliJ IDEA or Android Studio with Kotlin style settings. Use the "Reformat Code" feature ("Ctrl+Alt+L" or "Cmd+Option+L") to automatically format code.
### 6.2. Detekt
* **Recommendation:** Integrate Detekt into the build process to enforce code style rules and detect potential issues.
"""gradle
// build.gradle.kts
dependencies {
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.0")
}
//detekt.yml
formatting:
ChainWrapping:
active: true
wrap: true
style:
NamingRules:
active: true
"""
### 6.3. ktlint
* **Recommendation:** Use ktlint, another linting tool specifically for kotlin. It has built-in rules and formatting capabilities.
This document provides a comprehensive set of coding standards for Kotlin that fosters consistency, readability, and maintainability. By following these guidelines, development teams can ensure high-quality code and reduce potential errors. Remember to regularly update these standards to align with the latest Kotlin features and best practices.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Kotlin This document outlines the coding standards for component design in Kotlin, aiming to guide developers towards creating reusable, maintainable, and efficient components. These standards are specific to Kotlin and leverage its unique features and capabilities. The design recommendations are aligned with current Kotlin versions and best practices. ## 1. Component Definition and Scope ### 1.1 Standard: Define Clear Component Boundaries * **Do This:** Define clear boundaries for each component, specifying its responsibilities and interactions with other components. * **Don't Do This:** Create monolithic components that handle too many unrelated responsibilities, leading to increased complexity and reduced reusability. **Why:** Clear boundaries promote modularity, making components easier to understand, test, and maintain. It also facilitates independent development and deployment. **Example:** """kotlin // Good: Separating data fetching and UI logic interface UserRepository { fun getUser(id: String): User? } class UserViewModel(private val userRepository: UserRepository) { fun loadUser(id: String): User? { return userRepository.getUser(id) } } // Bad: Combining data fetching and UI logic in a single class class UserView { fun displayUser(id: String) { // Fetch user from database within the view class } } """ ### 1.2 Standard: Favor Composition over Inheritance * **Do This:** Use composition to combine behaviors from different components, making the relationships explicit and adaptable. When possible combine with interfaces. * **Don't Do This:** Rely heavily on inheritance, which can lead to tight coupling and the fragile base class problem. **Why:** Composition allows for greater flexibility and easier maintenance by decoupling components and giving the ability to swap out dependencies. **Example:** """kotlin // Good: Composition using interfaces interface Logger { fun log(message: String) } class ConsoleLogger : Logger { override fun log(message: String) { println("Console: $message") } } class FileLogger(private val filePath: String) : Logger { override fun log(message: String) { // Logic to write to the file println("File: $message") } } class UserService(private val logger: Logger) { fun createUser(name: String) { logger.log("Creating user: $name") } } // Usage (Dependency Injection): val consoleLogger = ConsoleLogger() val userServiceWithConsole = UserService(consoleLogger) userServiceWithConsole.createUser("Alice") val fileLogger = FileLogger("app.log") val userServiceWithFile = UserService(fileLogger) userServiceWithFile.createUser("Bob") // Bad: Inheritance open class BaseLogger { open fun log(message: String) { println("Base: $message") } } class UserServiceWithLogger : BaseLogger() { fun createUser(name: String) { log("Creating user: $name") } } """ ## 2. Designing Reusable Components ### 2.1 Standard: Adhere to the Single Responsibility Principle (SRP) * **Do This:** Ensure that each component has one and only one reason to change. * **Don't Do This:** Create classes or functions that perform multiple unrelated tasks. **Why:** SRP makes components easier to understand, test, and modify, leading to more robust and maintainable code. **Example:** """kotlin // Good: Separating user validation and repository access class UserValidator { fun isValidUsername(username:String): Boolean{ return username.matches(Regex("^[a-zA-Z0-9_]+$")) } } interface UserRepository { fun saveUser(user: User) } class User(val username: String){ } class UserService(private val userRepository: UserRepository, private val userValidator: UserValidator){ fun registerUser(username: String){ if(!userValidator.isValidUsername(username)){ println("invalid username") return } userRepository.saveUser(User(username)) } } """ ### 2.2 Standard: Use Interfaces and Abstract Classes Effectively * **Do This:** Define interfaces to represent contracts between components, allowing for flexible implementations and decoupling. Abstract classes should be used to define common behavior that can be inherited by concrete components. * **Don't Do This:** Overuse abstract classes when interfaces would suffice, or create interfaces with too many responsibilities. **Why:** Interfaces promote loose coupling, enabling different implementations of the same contract. Abstract classes provide a base implementation while allowing customization. **Example:** """kotlin // Good: Using an interface for a payment gateway interface PaymentGateway { fun charge(amount: Double, token: String): Boolean } class StripePaymentGateway : PaymentGateway { override fun charge(amount: Double, token: String): Boolean { // Stripe-specific implementation return true // Simulate success } } class PayPalPaymentGateway : PaymentGateway { override fun charge(amount: Double, token: String): Boolean { // PayPal-specific implementation return true // Simulate success } } class PaymentProcessor(private val paymentGateway: PaymentGateway) { fun processPayment(amount: Double, token: String): Boolean { return paymentGateway.charge(amount, token) } } """ ### 2.3 Standard: Favor Immutable Data Structures * **Do This:** Use immutable data structures, such as "data class" with "val" properties, whenever possible. * **Don't Do This:** Modify data structures in place without good reason, as this can lead to unexpected side effects and difficult-to-debug issues. **Why:** Immutable data structures simplify reasoning about the state of components and improve thread safety. **Example:** """kotlin // Good: Immutable data class data class Address(val street: String, val city: String, val zipCode: String) // Bad: Mutable data class class MutableAddress(var street: String, var city: String, var zipCode: String) """ ### 2.4 Standard: Use sealed classes for representing restricted class hierarchies * **Do This:** When a class represents a value from a limited set of options especially for states or events, use sealed classes that encapsulates all types. * **Don't Do This:** Use Enums when the enum options may contain state. Do not use open classes if you intend instances of other subclasses to be controlled by the base class **Why:** Sealed classes ensures type safety and exhaustive when used in "when" statements. They give exhaustive control over hierarchies. **Example:** """kotlin // Good: Sealed Classes for state management sealed class UIState { object Loading : UIState() data class Success(val data: String) : UIState() data class Error(val message: String) : UIState() } fun handleState(state: UIState) { when (state) { is UIState.Loading -> println("Loading...") is UIState.Success -> println("Data: ${state.data}") is UIState.Error -> println("Error: ${state.message}") } } // Bad: Enums when properties are needed. enum class Result { SUCCESS, FAILURE } """ ## 3. Leveraging Kotlin-Specific Features ### 3.1 Standard: Use Extension Functions Thoughtfully * **Do This:** Use extension functions to add functionality to existing classes without modifying their source code, but avoid overusing them. * **Don't Do This:** Add irrelevant or overly broad functionality to classes through extension functions. **Why:** Extension functions can improve code readability and maintainability by allowing you to add methods to classes as if they were originally part of the class. **Example:** """kotlin // Good: Extension function for validating email format fun String.isValidEmail(): Boolean { return this.matches(Regex("[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}")) } fun main() { val email = "test@example.com" if (email.isValidEmail()) { println("Valid email") } } """ ### 3.2 Standard: Utilize Data Classes for Value Objects * **Do This:** Use "data class" for creating classes that primarily hold data, taking advantage of features like automatic "equals()", "hashCode()", and "toString()" implementations. * **Don't Do This:** Use standard "class" when you only need to hold data. **Why:** "Data class" simplifies the creation of value objects by automatically generating boilerplate code. They also enhance null safety and data integrity. **Example:** """kotlin // Good: Data class for representing a point data class Point(val x: Int, val y: Int) fun main() { val p1 = Point(10, 20) val p2 = Point(10, 20) println(p1 == p2) // true (equals() is automatically generated) println(p1.toString()) // Point(x=10, y=20) (toString() is automatically generated) } """ ### 3.3 Standard: Employ Coroutines for Asynchronous Operations * **Do This:** Use Kotlin coroutines to manage asynchronous operations, making asynchronous code easier to read and write. * **Don't Do This:** Rely on callback-based asynchronous programming, which can lead to callback hell and reduced code readability. Avoid "Thread" when possible. **Why:** Coroutines provide a structured and efficient way to handle concurrency, improving the performance and responsiveness of your applications. **Example:** """kotlin import kotlinx.coroutines.* // Good: Using coroutines for asynchronous operations suspend fun fetchData(): String { delay(1000) // Simulate network delay return "Data from server" } fun main() = runBlocking { val data = withContext(Dispatchers.IO) { fetchData() } println("Received data: $data") } """ ### 3.4 Standard: Prefer the "use" function for resource management * **Do This:** Utilize the "use" function to automatically close resources after they are used, preventing memory leaks and ensuring proper cleanup. * **Don't Do This:** Manually manage resource closing without try-finally blocks, which can lead to resource leaks if exceptions occur. **Why:** The "use" function provides a concise and reliable way to handle resource management, reducing the risk of resource leaks. **Example:** """kotlin import java.io.FileReader // Good: Using the use function for resource management fun readFile(filePath: String) { FileReader(filePath).use { reader -> println(reader.readText()) } // The file reader is automatically closed after use } """ ### 3.5 Standard: Use Kotlin collections effectively * **Do This:** Leverage Kotlin's rich collection API for performing common operations on lists, sets, and maps, such as filtering, mapping, and reducing with null safety. * **Don't Do This:** Use traditional Java collection operations when Kotlin provides more concise and expressive alternatives or handle nullable collections inefficiently. **Why:** Kotlin collections simplify data manipulation and make code more readable and maintainable. **Example:** """kotlin // Good: Using Kotlin collection functions val numbers = listOf(1, 2, 3, 4, 5) val evenNumbers = numbers.filter { it % 2 == 0 } // [2, 4] val squaredNumbers = numbers.map { it * it } // [1, 4, 9, 16, 25] val sum = numbers.reduce { acc, i -> acc + i } // 15 //Null Safety and filtering optionals val optionalList = listOf("A", null, "B") val filteredList = optionalList.filterNotNull() // [A, B] """ ## 4. Performance Considerations ### 4.1 Standard: Avoid unnecessary object creation * **Do This:** Minimize object creation, especially in performance-critical sections of code. Consider using object pooling or caching to reuse objects. * **Don't Do This:** Create objects unnecessarily within loops or frequently called functions. **Why:** Object creation can be expensive, especially in memory-constrained environments. **Example:** """kotlin // Good: Caching objects when possible val cache = mutableMapOf<String, String>() fun getValue(key: String): String { return cache.getOrPut(key) { // Expensive operation to fetch the value "Value for $key" } } """ ### 4.2 Standard: Use inline functions for performance-critical code * **Do This:** Use "inline" functions for small, performance-critical functions, especially when using lambda expressions. * **Don't Do This:** Overuse inline functions for large functions, as this can increase code size and reduce performance. **Why:** Inline functions can reduce the overhead of function calls by inlining the function body directly into the calling code, resulting in improved performance. """kotlin // Good: Inline function for simple operations inline fun <T> measureTime(block: () -> T): T { val startTime = System.nanoTime() val result = block() val endTime = System.nanoTime() val duration = (endTime - startTime) / 1_000_000.0 // milliseconds println("Execution time: $duration ms") return result } fun main() { val result = measureTime { Thread.sleep(100) // Simulate some work "Operation completed" } println(result) } """ ### 4.3 Standard: Prioritize immutable collections for thread safety * **Do This:** Use "immutable" collections when dealing with multithreaded environment. * **Don't Do This:** Rely solely on Mutable collections especially those that aren't inherently threadsafe. **Why:** Immutability is key for reducing race conditions. """kotlin import kotlinx.coroutines.* import kotlin.system.measureTimeMillis fun main(): Unit = runBlocking { val numElements = 1000 val list = MutableList(numElements) { 0 } val mutex = kotlinx.coroutines.sync.Mutex() // Mutex for thread safety val timeTaken = measureTimeMillis { coroutineScope { for (i in 0 until numElements) { launch { mutex.withLock { // Safely update the list list[i] = i * 2 } } } } } println("Time taken: $timeTaken milliseconds") } """ ## 5. Error Handling ### 5.1 Standard: Handle exceptions appropriately * **Do This:** Use "try-catch" blocks to handle exceptions and prevent application crashes. Log exceptions for debugging purposes. Consider when specialized exception classes are needed. * **Don't Do This:** Ignore exceptions or catch generic "Exception" without proper handling. **Why:** Proper exception handling ensures that errors are gracefully handled and do not lead to application instability. """kotlin import java.io.FileNotFoundException import java.io.FileReader import java.io.IOException // Good: Handling exceptions with try-catch fun readFile(filePath: String) { try { FileReader(filePath).use { reader -> println(reader.readText()) } } catch (e: FileNotFoundException) { println("File not found: $filePath") } catch (e: IOException) { println("Error reading file: ${e.message}") } catch (e: Exception) { println("An unexpected error occurred: ${e.message}") } } """ ### 5.2 Standard: Use the Result type for controlled error handling * **Do This:** Use the "Result" type to represent operations that can either succeed or return an error, making error handling more explicit, particularly if you have an operation that should not throw an error. * **Don't Do This:** Rely solely on exceptions for representing errors, as this can make it difficult to reason about the possible outcomes of a function. **Why:** The ""Result"" type makes error handlings more controlled. You can manage successful/unsuccessful executions and handle them appropriately. """kotlin // Good: Using the Result type for error handling fun divide(a: Int, b: Int): Result<Int> { return if (b == 0) { Result.failure(IllegalArgumentException("Cannot divide by zero")) } else { Result.success(a / b) } } fun main() { val result = divide(10, 2) result .onSuccess { println("Result: $it") } .onFailure { println("Error: ${it.message}") } } """ ## 6. Testing ### 6.1 Standard: Write unit tests for all components * **Do This:** Write unit tests to verify the behavior of individual components in isolation, ensuring that they function as expected. Aim for high test coverage. * **Don't Do This:** Neglect to write unit tests, as this can lead to undetected bugs and reduced code quality. **Why:** Unit tests provide confidence in the correctness of components, making it easier to refactor and maintain the codebase. """kotlin import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test // Good: Unit test example using JUnit class Calculator { fun add(a: Int, b: Int): Int { return a + b } } class CalculatorTest { @Test fun testAdd() { val calculator = Calculator() assertEquals(5, calculator.add(2, 3)) } } """ ### 6.2 Standard: Use mocking frameworks for isolated testing * **Do This:** Use mocking frameworks like Mockito or Mockk to isolate components from their dependencies during testing. * **Don't Do This:** Perform integration tests instead of unit tests when testing individual components, as this can make it difficult to identify the source of failures. **Why:** Mocking allows you to test components in isolation by simulating the behavior of their dependencies, making tests more focused and reliable. ## 7. Documentation ### 7.1 Standard: Document all components and their APIs * **Do This:** Write clear and concise documentation for all components, including their purpose, inputs, outputs, and any relevant usage instructions. Use KDoc for generating API documentation. * **Don't Do This:** Neglect to document components, as this can make it difficult for other developers to understand and use them. **Why:** Component documentation facilitates knowledge sharing and makes it easier to maintain and evolve the codebase. """kotlin /** * A class that provides utility functions for string manipulation. * * @param separator The separator to use when joining strings. */ class StringUtilities(private val separator: String) { /** * Joins a list of strings into a single string using the specified separator. * * @param strings The list of strings to join. * @return The joined string. */ fun joinStrings(strings: List<String>): String { return strings.joinToString(separator) } } """ ### 7.2 Standard: Keep documentation up-to-date * **Do This:** Update documentation whenever components are modified, ensuring that it accurately reflects the current state of the codebase. * **Don't Do This:** Allow documentation to become outdated, as this can lead to confusion and errors. **Why:** Accurate and up-to-date documentation is essential for maintaining the integrity of the codebase and facilitating collaboration among developers. ## 8. Security Practices ### 8.1 Standard: Validate input data * **Do This:** Validate all input data to prevent injection attacks, data corruption, and other security vulnerabilities. * **Don't Do This:** Trust input data without validation, as this can expose your application to security risks. **Why:** Input validation is a critical security measure that helps prevent malicious data from entering your application. **Example:** """kotlin fun createUser(username: String, email: String) { // Sanitize the strings with whitelisting and trimming before use. val sanitizedUsername = username.filter { it.isLetterOrDigit() }.trim() val sanitizedEmail = email.filter { it.isLetterOrDigit() || it == '@' || it == '.' }.trim() if (sanitizedUsername.isEmpty() || sanitizedEmail.isEmpty()) { throw IllegalArgumentException("Invalid username or email") } // ... continue with user creation } """ ### 8.2 Standard: Handle sensitive data securely * **Do This:** Encrypt sensitive data at rest and in transit. Use secure storage mechanisms for storing credentials and other sensitive information. * **Don't Do This:** Store sensitive data in plain text or transmit it over insecure channels. **Why:** Secure handling of sensitive data is essential for protecting user privacy and preventing data breaches. These guidelines provide a foundation for building robust, maintainable, and secure Kotlin applications. Adhering to these standards will improve code quality, promote collaboration, and reduce the risk of errors and security vulnerabilities.
# State Management Standards for Kotlin This document outlines the coding standards for state management in Kotlin applications. Effective state management is crucial for building robust, maintainable, and performant applications. These standards aim to guide developers in choosing the appropriate techniques and patterns, minimizing complexity, and maximizing code clarity for Kotlin projects. ## 1. Architecture: Overall State Management Strategy ### 1.1. Standard: Centralized vs. Decentralized State **Do This:** * Prefer centralized state management for complex, interactive applications with intricate data dependencies. * Favor decentralized state management for smaller, simpler applications or modules within larger systems, where independent components manage their own data. **Don't Do This:** * Avoid globally mutable state without clear ownership and synchronization mechanisms, as it leads to unpredictable behavior and difficult debugging. * Don't mix centralized and decentralized approaches indiscriminately within the same module, as this increases complexity and hinders understanding. **Why:** * Centralized state simplifies tracking and debugging, especially when combined with unidirectional data flow. * Decentralized state enhances modularity, testability, and promotes reusable components. * Clear architectural boundaries improve collaboration and reduce the risk of introducing bugs through unintended state modifications. **Example (Centralized - Redux-inspired):** """kotlin // A simple state class data class AppState(val counter: Int = 0) // Actions to modify the state sealed class AppAction { object Increment : AppAction() object Decrement : AppAction() } // A reducer function that takes the current state and an action, and returns a new state fun reducer(state: AppState, action: AppAction): AppState { return when (action) { AppAction.Increment -> state.copy(counter = state.counter + 1) AppAction.Decrement -> state.copy(counter = state.counter - 1) } } // Sample usage (using a simple state holder) class StateHolder { private var _state: AppState = AppState() var state: AppState get() = _state private set(value) { _state = value } fun dispatch(action: AppAction) { state = reducer(state, action) // Notify listeners (e.g., UI updates) println("New State: $state") } } fun main() { val stateHolder = StateHolder() stateHolder.dispatch(AppAction.Increment) // Output: New State: AppState(counter=1) stateHolder.dispatch(AppAction.Decrement) // Output: New State: AppState(counter=0) } """ **Example (Decentralized - Using MVVM):** """kotlin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow // Data class representing the UI state of a component data class CounterUiState(val count: Int = 0) // ViewModel for a counter component class CounterViewModel { private val _uiState = MutableStateFlow(CounterUiState()) val uiState: StateFlow<CounterUiState> = _uiState fun increment() { _uiState.value = _uiState.value.copy(count = _uiState.value.count + 1) } fun decrement() { _uiState.value = _uiState.value.copy(count = _uiState.value.count - 1) } } import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() { val viewModel = CounterViewModel() // Observing the state: val job = CoroutineScope(Dispatchers.Default).launch { viewModel.uiState.collect { uiState -> println("Count: ${uiState.count}") } } viewModel.increment() // Output: Count: 1 viewModel.increment() // Output: Count: 2 viewModel.decrement() // Output: Count: 1 job.cancel() } """ ### 1.2. Standard: Unidirectional Data Flow (UDF) **Do This:** * Implement a unidirectional data flow pattern in your application, where state changes are triggered by explicit actions or events, and data flows in a single direction. Often paired with a centralized state manager. * Use immutable data structures to represent your application's state. Updates should create new instances rather than modifying existing ones. **Don't Do This:** * Avoid directly modifying state from multiple parts of the application, as this becomes difficult to track and debug. * Don't allow side effects within state update logic. State transformations are purely functional, based on existing state and triggered actions. **Why:** * UDF promotes predictable state management, simplifies debugging, and enables features like time-travel debugging. * Immutability eliminates the risk of unintended side effects and makes it easier to reason about data transformations. * Predictability leads to more testable code. **Example:** See the Redux-inspired example in Section 1.1. It demonstrates immutable state updated via explicitly dispatched actions. ### 1.3. Standard: State Management Libraries & Frameworks **Do This:** * Evaluate and use well-established state management libraries and frameworks such as ReduxKotlin, Reaktive, or libraries that build upon Kotlin Flows and StateFlows. * Choose a library that aligns with your application's complexity, team's expertise, and performance requirements. **Don't Do This:** * Avoid rolling your own state management solution unless you have a very specific use case and a deep understanding of state management principles. * Blindly adopt a library without considering its performance implications, dependencies, and long-term maintainability. **Why:** * Existing libraries provide robust implementations of common state management patterns, saving development time and reducing the risk of introducing bugs. * Libraries often include performance optimizations, debugging tools, and integration with other popular frameworks. * Using established libraries increases code consistency across different projects. **Example (Kotlin Coroutines StateFlow):** """kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // Representing a UI state with a count data class CounterState(val count: Int = 0) // A simple repository or data source class CounterRepository { private val _state = MutableStateFlow(CounterState()) val state: StateFlow<CounterState> = _state.asStateFlow() // Expose as read-only StateFlow fun increment() { _state.value = CounterState(count = _state.value.count + 1) } } //Simple Usage fun main() = runBlocking { val repository = CounterRepository() // Collect and observe the state (e.g., in a UI) val collectJob = launch { repository.state.collect { counterState -> println("Current count: ${counterState.count}") } } // Simulate user interactions (incrementing the counter) repository.increment() repository.increment() repository.increment() delay(100) // Give collectors time to process the update collectJob.cancel() // Stop collecting when done } """ ## 2. Data Flow & Reactivity ### 2.1. Standard: Kotlin Flows and StateFlows **Do This:** * Utilize "Flow" and "StateFlow" from "kotlinx.coroutines.flow" for managing asynchronous data streams and reactive state updates. * Use "StateFlow" for representing observable state holders that emit the current state and updates. * Employ "Flow" for event streams that represent a sequence of values over time. **Don't Do This:** * Avoid using "LiveData" outside of Android-specific code, as "Flow" and "StateFlow" are the recommended approach for most Kotlin projects. * Don't block the main (UI) thread when collecting from "Flow"s or "StateFlow"s. Use appropriate coroutine contexts ("Dispatchers.IO", "Dispatchers.Default") and "withContext" when necessary. **Why:** * "Flow" and "StateFlow" are Kotlin-first solutions that integrate seamlessly with coroutines, offering a clean and concise way to handle asynchronous operations and reactive updates. * They provide built-in support for backpressure, cancellation, and error handling. * "StateFlow" efficiently emits state updates and replays the latest value to new subscribers. **Example:** See the "CounterRepository" example above in section 1.3 demonstrating "StateFlow". **Example (Using Flow for events):** """kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simple event sealed class UIEvent { data class ShowMessage(val message: String) : UIEvent() object RefreshData : UIEvent() } class UIEventBus { private val _eventFlow = MutableSharedFlow<UIEvent>() val eventFlow: SharedFlow<UIEvent> = _eventFlow.asSharedFlow() // Read-only suspend fun postEvent(event: UIEvent) { _eventFlow.emit(event) } } fun main() = runBlocking { val eventBus = UIEventBus() // Simulate UI observing the EventFlow val collectJob = launch{ eventBus.eventFlow.collect { event -> when (event) { is UIEvent.ShowMessage -> println("Show Message: ${event.message}") UIEvent.RefreshData -> println("Refreshing Data") } } } eventBus.postEvent(UIEvent.ShowMessage("Hello from event bus!")) eventBus.postEvent(UIEvent.RefreshData) delay(100)// Wait to process messages collectJob.cancel() } """ ### 2.2. Standard: Immutability and Data Classes **Do This:** * Use Kotlin's "data class" to represent immutable state objects. This provides "equals", "hashCode", and "copy" implementations automatically. * Use the "copy()" method to create new instances of data classes when updating the state, ensuring immutability. * Declare properties within data classes as "val" to enforce immutability. **Don't Do This:** * Avoid using mutable data structures (e.g., "var" properties) inside your state classes, as this compromises the principles of immutability and UDF. * Don't modify existing state objects directly; always create and return a new copy with the updated values. **Why:** * Immutability simplifies reasoning about state changes and helps prevent unintended side effects. * Data classes provide concise and efficient ways to represent data and perform common operations on data objects. **Example:** """kotlin data class User(val id: Int, val name: String, val email: String) fun updateUserEmail(user: User, newEmail: String): User { return user.copy(email = newEmail) // Creates a new User instance } fun main() { val user1 = User(1, "Alice", "alice@example.com") val user2 = updateUserEmail(user1, "alice.new@example.com") println("User 1: $user1") // User 1: User(id=1, name=Alice, email=alice@example.com) println("User 2: $user2") // User 2: User(id=1, name=Alice, email=alice.new@example.com) } """ ### 2.3 Standard: Using "SharedFlow" **Do This:** * Use "SharedFlow" for broadcasting events or state updates to multiple subscribers. This is particularly useful for one-to-many communication scenarios or for handling side effects. * Configure the "replay" parameter of "SharedFlow" to replay a certain number of past events to new subscribers if necessary. * Use "SharingStarted" strategies when converting a "Flow" to a "SharedFlow" to control when upstream collection starts and stops, particularly for lifecycle-aware components. Use "SharingStarted.WhileSubscribed()" if you only want to collect when there is an active subscriber. **Don't Do This:** * Don't overuse "SharedFlow" where a "StateFlow" would be more suitable. "StateFlow" is specifically designed for holding state, while "SharedFlow" is designed for broadcasting events. * Don't forget to handle emitted values on the consuming end, as unhandled events are lost. * Don't use "SharedFlow" with unlimited replay cache if it can lead to memory leaks or performance issues. **Why:** * "SharedFlow" is a flexible and powerful tool for managing events and state in concurrent environments. It allows multiple subscribers to receive updates from a single source. * Configuration options allow fine-grained control over the flow's behavior, such as whether to replay past events to new subscribers or to buffer events when no subscribers are active. This flexibility caters to diverse application needs. **Example:** """kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val hotFlow = MutableSharedFlow<Int>(replay = 1) // Replay last emitted value //Create a cold flow for demonstration val coldFlow = flow { println("Starting flow execution") emit(1) delay(100) emit(2) } val sharedFlow: SharedFlow<Int> = coldFlow.shareIn( CoroutineScope(Dispatchers.Default), SharingStarted.WhileSubscribed() ) //Subscribing after the first value is emitted. coldFlow.collect{ println("processing ${it}") } val job1 = launch { sharedFlow.collect { value -> println("Collector 1: Received $value") } } //Emit initial value launch { delay(500) // println("Emitting") //delay(2000) println("Emitting") sharedFlow.collect { value -> println("Collector 2: Received $value") } } delay(1000)// Wait to process messages job1.cancel() } """ ## 3. Data Persistence ### 3.1. Standard: Repository Pattern **Do This:** * Implement the Repository pattern to abstract data access logic from the rest of the application. * Define interfaces for your repositories to enable easy testing and swapping of data sources. **Don't Do This:** * Avoid directly accessing data sources (e.g., databases, network APIs) from your UI components or business logic. * Don't tightly couple your data access layer to a specific data source implementation. **Why:** * The Repository pattern promotes separation of concerns, improves testability, and allows you to easily switch between different data sources without affecting other parts of your application. * It provides a centralized location for managing data access logic, making it easier to maintain and update. **Example:** """kotlin interface UserRepository { suspend fun getUser(id: Int): User? suspend fun saveUser(user: User) } class UserLocalRepository(private val userDao: UserDao) : UserRepository { override suspend fun getUser(id: Int): User? = userDao.getUserById(id) override suspend fun saveUser(user: User) { userDao.insert(user) } } //In-memory impl for testing class UserInMemoryRepository : UserRepository{ private val users = mutableMapOf<Int,User>() override suspend fun getUser(id: Int): User? = users[id] override suspend fun saveUser(user: User) { users[user.id] = user } } //Example DAO (Using Room in Android) - Example for demo only interface UserDao { //Example calls, doesn't need to represent actual Room code fun getUserById(id: Int): User? fun insert(user: User) } """ ### 3.2. Standard: Caching **Do This:** * Implement caching strategies to reduce network requests and improve application performance. * Use in-memory caching for frequently accessed data, and disk-based caching for larger datasets. * Invalidate caches when data changes to ensure data consistency. **Don't Do This:** * Avoid caching sensitive data without proper encryption and access control. * Don't cache data indefinitely; set appropriate expiration times to prevent stale data. **Why:** * Caching can significantly improve application responsiveness and reduce network usage. * Well-designed caching strategies can enhance the user experience, particularly in offline or low-bandwidth environments. **Example (Simple In-Memory Cache with Expiry):** """kotlin import kotlinx.coroutines.* import java.util.concurrent.ConcurrentHashMap class SimpleCache<K, V>(private val expiryMillis: Long = 60000) { // 1 minute expiration private val cache = ConcurrentHashMap<K, CacheEntry<V>>() data class CacheEntry<V>(val value: V, val expiryTime: Long) fun get(key: K): V? { val entry = cache[key] return if (entry != null && entry.expiryTime > System.currentTimeMillis()) { entry.value } else { cache.remove(key) // Remove expired entry null } } fun put(key: K, value: V) { cache[key] = CacheEntry(value, System.currentTimeMillis() + expiryMillis) } fun invalidate(key:K){ cache.remove(key) } } // Sample Usage: fun main() = runBlocking { val cache = SimpleCache<String,String>() cache.put("myKey","Some value") println(cache.get("myKey")) delay(70000) // Wait longer than the expiry time println(cache.get("myKey"))//Will print null } """ ## 4. Error Handling and State Reconciliation ### 4.1 Standard: Error handling within StateFlow emission **Do This:** * When using StateFlow, handle exceptions gracefully within the emission block (e.g. the "emit" call). * Use try-catch blocks to catch specific exceptions and update state accordingly. Emit error states, such as "data class ErrorState(val exception: Exception, val recoveryAction: () -> Unit)". **Don't Do This:** * Allow exceptions to propagate unhandled. Unhandled exceptions can lead to app crashes or undefined UI states. **Why:** * StateFlow must always emit a value, even when operations fail. Propagating exceptions will break the state stream. """kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* data class MyState(val data: String? = null, val isLoading: Boolean = false, val error: String? = null) fun myApiCall():String{ //Simulate a network call Thread.sleep(100) if (true) { throw IllegalStateException("Simulated API Error") } return "API Data" } class MyViewModel{ private val _state = MutableStateFlow(MyState()) val state: StateFlow<MyState> = _state.asStateFlow() fun fetchData(){ CoroutineScope(Dispatchers.IO).launch { _state.emit(MyState(isLoading = true)) try{ val result = myApiCall() _state.emit(MyState(data = result))} catch (e:Exception){ _state.emit(MyState(error = "Error: ${e.message}")) } finally { _state.emit(MyState(isLoading = false)) } } } } fun main() = runBlocking { val viewModel = MyViewModel() val job = launch { viewModel.state.collect { state -> println("State: $state") } } viewModel.fetchData() delay(1000) job.cancel() } """ This coding standards document provides a comprehensive set of guidelines for developing maintainable, performant, and secure Kotlin applications that effectively manage state. Adhering to these standards will help improve code quality, reduce errors, and facilitate collaboration within development teams.
# Core Architecture Standards for Kotlin This document outlines the core architectural standards for Kotlin development. These standards are designed to promote maintainability, scalability, testability, and overall code quality in Kotlin projects. They are based on modern Kotlin practices and leverage the language's key features for optimal design. ## 1. Architectural Patterns ### 1.1. Model-View-ViewModel (MVVM) **Standard:** Employ the MVVM pattern for UI-driven applications, especially in Android development, to separate UI logic, data presentation, and data management. **Why:** MVVM facilitates a clear separation of concerns, making the UI code more testable and maintainable. It also allows for easier data binding and UI updates. **Do This:** * Use "LiveData", "StateFlow", or "SharedFlow" from "kotlinx.coroutines" to manage observable data streams between the ViewModel and the View. * Keep the View (Activity/Fragment/Compose Composable) as passive observers, primarily responsible for displaying data and forwarding user actions to the ViewModel. * Delegate all data manipulation and business logic to the ViewModel. **Don't Do This:** * Avoid placing business logic directly within Activities or Fragments. * Do not perform long-running operations on the main UI thread. Use coroutines for background tasks. **Example (Android with Compose):** """kotlin // ViewModel import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import androidx.lifecycle.viewModelScope class UserViewModel : ViewModel() { private val _userName = MutableStateFlow("Initial Name") val userName: StateFlow<String> = _userName fun updateName(newName: String) { viewModelScope.launch { _userName.emit(newName) } } } // View (Composable) import androidx.compose.runtime.collectAsState import androidx.compose.runtime.Composable @Composable fun UserScreen(viewModel: UserViewModel = UserViewModel()) { val name = viewModel.userName.collectAsState() Text(text = "Hello, ${name.value}!") Button(onClick = { viewModel.updateName("New Name") }) { Text("Update Name") } } """ ### 1.2. Clean Architecture **Standard:** Adopt Clean Architecture principles, especially for complex applications, to maximize testability, maintainability, and independence from frameworks. **Why:** Clean Architecture promotes a highly decoupled system, where the core business logic is independent of external dependencies (UI, database, frameworks). This makes the code easier to test, change, and adapt to new technologies. **Do This:** * Structure the project into layers: Entities (business objects), Use Cases (application logic), Interface Adapters (presenters, gateways), and Frameworks & Drivers (UI, database). * Ensure dependencies flow inward, meaning that inner layers should not depend on outer layers. * Use dependency injection to provide implementations of interfaces in outer layers to the inner layers. **Don't Do This:** * Avoid direct dependencies between the UI and the data layer. * Do not mix business logic with framework-specific code. **Example (Simplified Layers):** """kotlin // Entities (data models) data class User(val id: Int, val name: String) // Use Cases (business logic) interface GetUserUseCase { suspend operator fun invoke(userId: Int): User } class GetUserUseCaseImpl(private val userRepository: UserRepository) : GetUserUseCase { override suspend operator fun invoke(userId: Int): User { return userRepository.getUser(userId) } } // Interface Adapters (repositories) interface UserRepository { suspend fun getUser(userId: Int): User } // Frameworks & Drivers (data source) class RemoteUserRepository(private val apiService: ApiService) : UserRepository { override suspend fun getUser(userId: Int): User { return apiService.fetchUser(userId) } } // Usage (with Ktor) in your framework layer import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* import kotlinx.serialization.Serializable @Serializable data class UserDto(val id: Int, val name: String) interface ApiService { suspend fun fetchUser(userId: Int): User } class ApiServiceImpl : ApiService { private val client = HttpClient(CIO) { install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { io.ktor.serialization.kotlinx.json.json() } } override suspend fun fetchUser(userId: Int): User { val userDto: UserDto = client.get("https://example.com/users/$userId").body() return User(userDto.id, userDto.name) } } """ ### 1.3. Modular Architecture **Standard:** Decompose large projects into smaller, independent modules to improve build times, code reusability, and team collaboration. **Why:** Modularization reduces the impact of changes, allows for parallel development, and facilitates feature isolation. It aligns well with the principles of Clean Architecture. **Do This:** * Identify logical boundaries within the application (e.g., feature modules, data modules, UI modules). * Define clear interfaces between modules. * Use Gradle's "implementation", "api", and "compileOnly" dependencies to control module visibility. **Don't Do This:** * Avoid circular dependencies between modules. This will dramatically increase build times. * Do not expose internal implementation details of a module to other modules directly. **Example (Gradle settings.gradle.kts):** """kotlin rootProject.name = "MyApplication" include(":app") include(":feature-a") include(":feature-b") include(":data") """ **Example (Gradle build.gradle.kts for :app):** """kotlin dependencies { implementation(project(":feature-a")) implementation(project(":data")) } """ ## 2. Project Structure and Organization ### 2.1. Package Naming **Standard:** Use reverse domain name notation for package names (e.g., "com.example.myapp"). **Why:** Prevents naming conflicts and ensures uniqueness of packages across different projects and organizations. **Do This:** * Use descriptive package names that reflect the functionality of the contained classes. * Organize packages hierarchically based on logical groupings of code. **Don't Do This:** * Avoid using generic package names like "utils" or "helpers" without further specificity. * Do not create excessively deep package hierarchies. **Example:** """kotlin package com.example.myapp.ui.screens.home // Classes related to the Home screen UI """ ### 2.2. File Organization **Standard:** Structure source files to contain related classes, functions, and properties, focusing on a single cohesive unit of functionality. **Why:** Improves code readability and maintainability by grouping related elements together. **Do This:** * Place a single public top-level class, interface, or enum in its own file named after the class. * Group related top-level functions or extensions in a file that summarizes the functionality. * Keep files concise and focused on a single purpose. **Don't Do This:** * Avoid having too many classes or functions in a single file, making it difficult to navigate and understand. * Do not mix unrelated functionalities in the same file. **Example:** """kotlin // HomeActivity.kt package com.example.myapp.ui.screens.home import android.os.Bundle import androidx.activity.ComponentActivity class HomeActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... } } // DateUtils.kt (top-level functions) package com.example.myapp.utils import java.time.LocalDate import java.time.format.DateTimeFormatter fun formatDate(date: LocalDate, pattern: String = "yyyy-MM-dd"): String { val formatter = DateTimeFormatter.ofPattern(pattern) return date.format(formatter) } """ ### 2.3. Layered Directory Structure **Standard:** Implement a layered directory structure that mirrors the chosen architectural pattern (e.g., MVVM, Clean Architecture). **Why:** Provides a clear and consistent organization that makes it easy to locate and understand different parts of the application. **Do This:** * Create separate directories for each architectural layer (e.g., "ui", "domain", "data"). * Subdivide directories further based on features or modules. **Don't Do This:** * Avoid a flat directory structure, as it makes it difficult to find code in larger projects. * Do not mix components from different layers in the same directory. **Example:** """ app/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ ├── com/example/myapp/ │ │ │ │ ├── ui/ # UI Layer (Activities, Fragments, Composables) │ │ │ │ │ ├── home/ │ │ │ │ │ │ ├── HomeScreen.kt │ │ │ │ │ │ ├── HomeViewModel.kt │ │ │ │ ├── domain/ # Domain Layer (Use Cases, Entities) │ │ │ │ │ ├── GetUsersUseCase.kt │ │ │ │ │ ├── User.kt │ │ │ │ ├── data/ # Data Layer (Repositories, Data Sources) │ │ │ │ │ ├── UserRepository.kt │ │ │ │ │ ├── RemoteDataSource.kt """ ## 3. Concurrency & Asynchronous Programming ### 3.1. Kotlin Coroutines **Standard:** Use Kotlin Coroutines for asynchronous programming to handle background tasks, network operations, and UI updates without blocking the main thread. **Why:** Coroutines provide a lightweight, efficient, and structured way to write asynchronous code. They simplify complex asynchronous logic and improve code readability. **Do This:** * Use "suspend" functions to mark functions that can be paused and resumed. * Use "CoroutineScope" and "launch" or "async" to start coroutines. * Use "withContext" to switch between different "Dispatchers" (e.g., "Dispatchers.IO" for I/O operations, "Dispatchers.Main" for UI updates). **Don't Do This:** * Avoid blocking the main thread with long-running operations. * Do not use "Thread.sleep()" or similar blocking calls in coroutines. **Example:** """kotlin import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { // Background task withContext(Dispatchers.IO) { delay(1000) println("Background task completed") } // Update UI (on main thread) withContext(Dispatchers.Main) { println("Updating UI") } } println("Main thread continues") job.join() // Wait for the job to complete println("Done") } """ ### 3.2. Flows **Standard:** Use "Flow" from "kotlinx.coroutines" for asynchronous streams of data, especially when handling reactive data sources or continuous updates. **Why:** Flows provide a reactive stream API with backpressure support, allowing you to handle data updates efficiently and react to changes over time. **Do This:** * Create "Flow"s using "flow {}" builder. * Use operators like "map", "filter", "collect", and "debounce" to transform and process the data stream. * Handle exceptions using "catch" operator or "try-catch" blocks within the "flow {}" builder. **Don't Do This:** * Avoid blocking the "collect" terminal operator, as it will block the upstream flow. * Do not perform CPU-intensive operations directly within the "collect" block. **Example:** """kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val flow = flow { for (i in 1..5) { delay(500) emit(i) } } flow.filter { it % 2 == 0 } .map { it * 2 } .collect { value -> println("Collected: $value") } } """ ### 3.3. Structured Concurrency **Standard:** Apply structured concurrency principles to manage coroutine lifecycles and prevent memory leaks. **Why:** Structured concurrency ensures that all coroutines launched within a scope are properly tracked and cancelled when the scope is cancelled, preventing orphaned coroutines and resource leaks. **Do This:** * Use "coroutineScope {}" and "supervisorScope {}" to define structured concurrency scopes. * Use "viewModelScope" in Android ViewModels to automatically cancel coroutines when the ViewModel is cleared. * Prefer "async {}" for parallel execution within a scope. **Don't Do This:** * Avoid launching coroutines without a proper scope, as they can become orphaned and lead to memory leaks. * Do not ignore exceptions thrown by child coroutines within a scope. **Example:** """kotlin import kotlinx.coroutines.* fun main() = runBlocking { coroutineScope { val job1 = launch { delay(1000) println("Job 1 completed") } val job2 = async { delay(500) println("Job 2 completed") "Result from Job 2" } println("Jobs launched") job1.join() val result = job2.await() println("Result: $result") } println("Coroutine scope finished") } """ ## 4. Dependency Injection ### 4.1. Hilt (Android) or Koin (Multiplatform) **Standard:** Use a dependency injection framework like Hilt (for Android) or Koin (for multiplatform projects) to manage dependencies and promote loose coupling. **Why:** Dependency injection makes code more testable, maintainable, and reusable by decoupling components from their dependencies. It also simplifies configuration and reduces boilerplate code. **Do This (Hilt - Android):** * Annotate classes with "@AndroidEntryPoint" or "@HiltViewModel" to inject dependencies. * Use "@Inject" annotation to request dependencies in constructors or fields. * Define modules annotated with "@Module" and "@InstallIn" to provide dependencies. * Use "@Binds" as needed for interface bindings within Hilt Modules. **Do This (Koin - Multiplatform):** * Define modules via the "module" keyword * Use "single", "factory" or "get()" as needed for dependency declarations * Load Modules using "startKoin" at the application level **Don't Do This:** * Avoid manually creating and managing dependencies within classes. Dependency injection frameworks are specifically designed for this. * Do not overuse dependency injection for trivial dependencies, as it can add unnecessary complexity. **Example (Hilt - Android):** """kotlin // Application class import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class MyApplication : Application() // ViewModel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class MyViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() { // ... } // Repository interface UserRepository { fun getUsers(): List<String> } class UserRepositoryImpl @Inject constructor(private val apiService: ApiService) : UserRepository { override fun getUsers(): List<String> { return apiService.fetchUsers() } } // API Service interface ApiService { fun fetchUsers(): List<String> } class ApiServiceImpl @Inject constructor() : ApiService { override fun fetchUsers(): List<String> { // Example, in Real world usage Ktor or Retrofit would be used return listOf("user1", "user2") } } // Hilt Module import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideApiService(): ApiService = ApiServiceImpl() @Provides @Singleton fun provideUserRepository(apiService: ApiService): UserRepository = UserRepositoryImpl(apiService) } """ **Example (Koin - Multiplatform):** """kotlin // Common module definition (shared by all targets) import org.koin.core.context.startKoin import org.koin.dsl.module val commonModule = module { single<MyService> { MyServiceImpl() } factory { MyComponent(get()) } } fun initKoin() { startKoin { modules(commonModule) } } interface MyService { fun doSomething(): String } class MyServiceImpl: MyService { override fun doSomething(): String { return "Hello from MyService" } } class MyComponent(private val myService: MyService) { fun printMessage() { println(myService.doSomething()) } } """ ### 4.2. Constructor Injection **Standard:** Prefer constructor injection over field injection or setter injection. **Why:** Constructor injection makes dependencies explicit and visible, improving code clarity and testability. It also ensures that all required dependencies are available when the object is created. **Do This:** * Declare all dependencies as constructor parameters. * Use default values for optional dependencies. **Don't Do This:** * Avoid using field injection ("@Inject lateinit var") unless absolutely necessary (e.g., in Android Activities/Fragments with Hilt). * Do not use setter injection, as it makes it harder to track dependencies and can lead to runtime errors if dependencies are not set. **Example:** """kotlin class MyClass(private val dependency1: Dependency1, private val dependency2: Dependency2? = null) { // ... } """ ## 5. Error Handling ### 5.1. Exceptions **Standard:** Use exceptions to handle exceptional situations and unexpected errors. **Why:** Exceptions provide a structured way to handle errors and prevent application crashes. They allow you to separate error-handling logic from normal program flow. **Do This:** * Throw exceptions when an error occurs that cannot be handled locally. * Use "try-catch" blocks to handle exceptions and gracefully recover from errors. * Create custom exception classes for specific error scenarios. **Don't Do This:** * Avoid using exceptions for normal program flow or control. * Do not catch exceptions and ignore them without logging or handling them appropriately. **Example:** """kotlin class MyException(message: String) : Exception(message) fun processData(data: String) { try { if (data.isEmpty()) { throw MyException("Data is empty") } // ... } catch (e: MyException) { println("Error: ${e.message}") // Handle the exception } finally { // Optional: Perform cleanup tasks } } """ ### 5.2. Result Type **Standard:** Consider using the "Result" type returned from functions that may encounter recoverable errors *without* throwing exceptions, especially when errors are expected outcomes. **Why:** Using Result type promotes explicit handling of success and failure scenarios, making code more robust and easier to reason about. It is especially useful for operations where the error is not truly exceptional but rather is a valid outcome. **Do This:** * Return a "Result<T>" from functions where T is the successful return type, and an error condition may occur * Use ".onSuccess{}" and ".onFailure{}" to handle results. **Don't Do This:** * Overuse for *all* error conditions, especially truly exceptional ones that should throw Exceptions. **Example** """kotlin fun divide(a: Int, b: Int): Result<Int> { return if (b == 0) { Result.failure(IllegalArgumentException("Cannot divide by zero")) } else { Result.success(a / b) } } fun main() { val result = divide(10, 2) result.onSuccess { println("Result: $it") }.onFailure { println("Error: ${it.message}") } val result2 = divide(5, 0) result2.onSuccess { println("Result: $it") }.onFailure { println("Error: ${it.message}") } } """ ## 6. Testing ### 6.1. Unit Tests **Standard:** Write unit tests for all critical classes and functions to verify their behavior in isolation. **Why:** Unit tests provide confidence in the correctness of the code and help prevent regressions. They also make it easier to refactor and maintain the code. **Do This:** * Use a testing framework like JUnit, Mockito, or Turbine. * Write tests that cover all possible scenarios and edge cases. * Use mocks and stubs to isolate the code under test from its dependencies. * Follow the AAA (Arrange, Act, Assert) pattern for structuring test cases. **Don't Do This:** * Avoid writing tests that are too tightly coupled to the implementation details of the code. * Do not skip writing tests for complex or critical parts of the application. **Example (JUnit & Mockito):** """kotlin import org.junit.jupiter.api.Test import org.mockito.Mockito.* import kotlin.test.assertEquals class MyClassTest { @Test fun "test calculateSum"() { val dependency = mock(MyDependency::class.java) "when"(dependency.getValue()).thenReturn(5) val myClass = MyClass(dependency) val result = myClass.calculateSum(3) assertEquals(8, result) verify(dependency).getValue() } } interface MyDependency { fun getValue(): Int } class MyClass(private val dependency: MyDependency) { fun calculateSum(num: Int): Int { return num + dependency.getValue() } } """ ### 6.2. Integration Tests **Standard:** Write integration tests to verify the interaction between different parts of the application. **Why:** Integration tests ensure that the different components of the application work together correctly. They can uncover issues that are not caught by unit tests. **Do This:** * Test the integration between different modules or layers of the application. * Use real or in-memory databases for integration tests. * Test the interaction with external systems (e.g., APIs) using mock servers. **Don't Do This:** * Avoid writing integration tests that are too broad or cover too many components at once. * Do not skip writing integration tests for critical integration points. These architectural standards provide a solid foundation for building maintainable, scalable, and testable Kotlin applications. Adhering to these guidelines will promote consistency, improve code quality, and facilitate collaboration within development teams. As Kotlin evolves, continue to review and refine these standards to leverage the latest language features and best practices.
# Testing Methodologies Standards for Kotlin This document outlines the testing methodologies standards for Kotlin, aiming to ensure high-quality, maintainable, and reliable code. It covers unit, integration, and end-to-end testing strategies specific to Kotlin, emphasizing modern approaches and patterns. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) **Do This:** Embrace Test-Driven Development (TDD) where possible. Write tests *before* implementing the code. This helps clarify requirements, improves design, and ensures testability. **Don't Do This:** Avoid writing tests *after* the code is implemented as an afterthought, which often leads to less comprehensive and less effective tests. **Why:** TDD drives better design, reduces defects, and improves code maintainability by ensuring that every piece of code has a corresponding test. **Code Example:** """kotlin // Red (Test) import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class StringCalculatorTest { @Test fun "add should return 0 for an empty string"() { assertEquals(0, StringCalculator().add("")) } } // Green (Implementation) class StringCalculator { fun add(input: String): Int { return 0 // Initial implementation to pass the test } } // Refactor (Improved Implementation) class StringCalculator { fun add(input: String): Int { if (input.isEmpty()) { return 0 } return input.toInt() // Incomplete implementation to show the TDD cycle } } """ ### 1.2. Test Pyramid **Do This:** Adhere to the test pyramid: many unit tests, fewer integration tests, and even fewer end-to-end tests. **Don't Do This:** Invert the pyramid by having a large number of end-to-end tests at the expense of unit tests. **Why:** Unit tests are faster and cheaper to run, providing rapid feedback. Integration and end-to-end tests are slower, more brittle, and more costly. ### 1.3. FIRST Principles **Do This:** Ensure tests are: * **F**ast: Tests should run quickly to encourage frequent execution. * **I**solated: Tests should not depend on each other. * **R**epeatable: Tests should produce the same results every time. * **S**elf-Validating: Tests should automatically determine if they passed or failed. * **T**horough: Tests should cover all important scenarios. **Don't Do This:** Write tests that are slow, interdependent, non-repeatable, or require manual validation. **Why:** These principles ensure that tests are reliable, efficient, and provide accurate feedback. ## 2. Unit Testing in Kotlin ### 2.1. Frameworks and Libraries **Do This:** Use JUnit Jupiter (JUnit 5) for standard unit testing. Consider using MockK or Mockito-Kotlin for mocking dependencies. Use AssertJ or Kotest for more expressive assertions. Leverage KotlinTest for property-based testing. **Don't Do This:** Rely on outdated testing libraries or reinvent the wheel when excellent libraries are available. **Why:** These libraries offer powerful features, improved syntax, and better integration with Kotlin. **Code Example (JUnit 5 and MockK):** """kotlin import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class UserServiceTest { @Test fun "getUserName should return correct name"() { val userRepository = mockk<UserRepository>() every { userRepository.findById(1) } returns User(1, "Alice") val userService = UserService(userRepository) val userName = userService.getUserName(1) assertEquals("Alice", userName) verify { userRepository.findById(1) } } } interface UserRepository { fun findById(id: Int): User? } data class User(val id: Int, val name: String) class UserService(private val userRepository: UserRepository) { fun getUserName(id: Int): String? { return userRepository.findById(id)?.name } } """ **Code Example (KotlinTest):** """kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.data.forAll import io.kotest.data.row import io.kotest.matchers.shouldBe class StringCalculatorKotestTest : StringSpec({ "String Calculator Tests" { forAll( row("", 0), row("1", 1), row("1,2", 3) ) { input, expected -> StringCalculator().add(input) shouldBe expected } } }) """ ### 2.2. Mocking Strategies **Do This:** Use dependency injection (constructor injection) heavily to enable easy mocking. Employ interfaces to define contracts for dependencies. Use mocking frameworks like MockK for creating mock objects. Leverage Kotlin's null safety to handle potential nulls gracefully. **Don't Do This:** Directly instantiate dependencies within classes, making them hard to mock. Avoid using mutable state in dependencies, as this can complicate mocking. **Why:** Dependency injection and mocking ensure that you are testing the unit in isolation, without external influences. **Code Example (MockK with Null Safety):** """kotlin import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test class UserServiceTest { @Test fun "getUserName should return null when user is not found"() { val userRepository = mockk<UserRepository>() every { userRepository.findById(1) } returns null val userService = UserService(userRepository) val userName = userService.getUserName(1) assertNull(userName) } } """ ### 2.3. Assertion Styles **Do This:** Use expressive assertion libraries like AssertJ or implement custom matchers in KotlinTest for clear and readable test results. Utilize Kotlin's extension functions to build custom assertions. **Don't Do This:** Rely solely on basic JUnit assertions, which can be less informative. **Why:** Expressive assertions make tests easier to understand and debug. **Code Example (AssertJ):** """kotlin import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class StringCalculatorTest { @Test fun "add should return correct sum"() { val result = StringCalculator().add("2,3") assertThat(result).isEqualTo(5) } } """ ### 2.4 Handling Coroutines in Unit Tests **Do This:** Use "runBlocking" or "runTest" (from "kotlinx-coroutines-test") in tests to execute suspending functions synchronously. Use "TestCoroutineDispatcher" to control virtual time for testing time-based coroutines. Mock suspending functions using MockK or similar frameworks. **Don't Do This:** Neglect to properly handle coroutines in tests, leading to flaky or unpredictable results. Attempt to use "Thread.sleep", which blocks the thread instead of suspending the coroutine. **Why:** Proper handling of coroutines ensures deterministic and reliable testing of asynchronous code. **Code Example (using "runTest"):** """kotlin import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class UserRepositoryTest { @Test fun "getUserNameAsync should return correct name"() = runTest { val userRepository = mockk<UserRepository>() every { userRepository.findByIdAsync(1) } returns User(1, "Alice") val userService = UserService(userRepository) val userName = userService.getUserNameAsync(1) assertEquals("Alice", userName) } } interface UserRepository { suspend fun findByIdAsync(id: Int): User? } class UserService(private val userRepository: UserRepository) { suspend fun getUserNameAsync(id: Int): String? { return userRepository.findByIdAsync(id)?.name } } """ ### 2.5 Data Classes and Tests **Do This:** Embrace data classes for representing test input and expected output. Leverage Kotlin's "copy()" function to easily create variations of test data. Use Kotlin's "==" operator for comparing data class instances. **Don't Do This:** Manually create and compare complex objects, which is error-prone and verbose. **Why:** Data classes simplify the creation and comparison of test data, leading to cleaner and more maintainable tests. **Code Example:** """kotlin import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test data class User(val id: Int, val name: String, val email: String) class UserTest { @Test fun "copy should create a new user with updated email"() { val originalUser = User(1, "Alice", "alice@example.com") val updatedUser = originalUser.copy(email = "alice.new@example.com") assertEquals(1, updatedUser.id) assertEquals("Alice", updatedUser.name) assertEquals("alice.new@example.com", updatedUser.email) } } """ ### 2.6. Property-Based Testing **Do This:** Consider using property-based testing frameworks such as KotlinCheck, Kotest property testing, or jqwik to automatically generate a wide range of inputs and verify that your code satisfies certain properties. **Don't Do This:** Rely solely on example-based tests, which may not cover all edge cases or unexpected inputs. **Why:** Property-based testing can uncover subtle bugs and edge cases that traditional unit tests might miss. **Code Example (Kotest Property Testing):** """kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.data.row import io.kotest.property.Arb import io.kotest.property.arbitrary.int import io.kotest.property.checkAll import io.kotest.matchers.shouldBe class StringCalculatorPropertyTest : StringSpec({ "add should return the correct sum of two integers" { checkAll(Arb.int(), Arb.int()) { a, b -> StringCalculator().add("$a,$b") == a + b } } }) """ ### 2.7 Handling Exceptions **Do This:** Use "assertThrows" (JUnit 5) or "shouldThrow" (Kotest) to verify that specific exceptions are thrown under expected conditions. **Don't Do This:** Neglect to test exception handling logic, which can lead to unexpected behavior and application crashes. **Why:** Properly testing exception handling ensures that your application gracefully handles errors and unexpected situations. **Code Example (JUnit 5):** """kotlin import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test class StringCalculatorTest { @Test fun "add should throw IllegalArgumentException for negative numbers" () { assertThrows(IllegalArgumentException::class.java) { StringCalculator().add("-1,2") } } } """ ## 3. Integration Testing in Kotlin ### 3.1. Database Integration Testing **Do This:** Use an embedded database like H2 or Testcontainers for integration tests involving databases. Seed the database with test data before running tests. Verify that data is correctly persisted and retrieved. Use transaction management to roll back changes after each test. **Don't Do This:** Run integration tests against a production database, which can lead to data corruption. Skip database testing, which can mask critical data access issues. **Why:** Database integration tests ensure that your application correctly interacts with the database layer. **Code Example (Testcontainers with PostgreSQL):** """kotlin import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers import java.sql.DriverManager @Testcontainers class DatabaseIntegrationTest { @Container val postgres = PostgreSQLContainer("postgres:13") @Test fun "test database interaction"() { val jdbcUrl = postgres.jdbcUrl val username = postgres.username val password = postgres.password val connection = DriverManager.getConnection(jdbcUrl, username, password) val statement = connection.createStatement() // Create table and insert some data statement.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))") statement.execute("INSERT INTO users (name) VALUES ('Alice')") // Query the data and verify val resultSet = statement.executeQuery("SELECT COUNT(*) FROM users") resultSet.next() val count = resultSet.getInt(1) assertEquals(1, count) } } """ ### 3.2. API Integration Testing **Do This:** Use a testing framework like HttpTest or MockWebServer to simulate external API calls. Verify that your application correctly sends requests and processes responses. Use data-driven tests to cover different API scenarios. **Don't Do This:** Directly call external APIs during integration tests, which can be unreliable and slow. Skip API integration testing, which is critical for applications that rely on external services. **Why:** API integration tests ensure that your application correctly interacts with external APIs. ### 3.3. Spring Boot Integration Testing **Do This:** Leverage Spring's "@SpringBootTest" annotation to start a full application context for integration tests. Use "@Autowired" to inject dependencies into your test class. Use "@MockBean" to replace specific beans with mock implementations. **Don't Do This:** Write integration tests that are tightly coupled to specific implementation details of your application. Avoid unnecessary mocking, which can reduce the value of integration tests. **Why:** Spring Boot integration tests allow you to verify that the different components of your application work together correctly within the Spring ecosystem. **Code Example (Spring Boot Integration Test):** """kotlin import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.mock.mockito.MockBean @SpringBootTest class UserIntegrationTest { @Autowired private lateinit var userService: UserService @MockBean private lateinit var userRepository: UserRepository @Test fun "getUserName should return correct name from mocked repository"() { // Setup Mock org.mockito.Mockito."when"(userRepository.findById(1)).thenReturn(User(1, "Bob")) val userName = userService.getUserName(1) assertEquals("Bob", userName) } } """ ## 4. End-to-End (E2E) Testing in Kotlin ### 4.1. Frameworks **Do This:** Use frameworks like Selenium, Appium (for mobile apps), or Cypress (for web apps) for E2E testing. **Don't Do This:** Neglect E2E tests altogether. Also, writing too many E2E tests can make the overall testing suite too slow and brittle. **Why:** E2E tests validate the entire system from the user's perspective, ensuring that all components work together correctly. ### 4.2. Page Object Model (POM) **Do This:** Use the Page Object Model (POM) to structure your E2E tests, encapsulating the UI elements and interactions for each page in separate classes. This makes tests more readable, maintainable, and reusable. **Don't Do This:** Directly manipulate UI elements within test methods, which makes tests brittle and hard to maintain. **Why:** POM separates test logic from UI locators, improving maintainability. ### 4.3. Test Data Management **Do This:** Use a dedicated test data management strategy, such as seeding the database with test data or creating test users and accounts. **Don't Do This:** Rely on production data for E2E tests. This can lead to data corruption and unpredictable test results. **Why:** Proper test data management ensures that E2E tests are repeatable and reliable. ### 4.4. Environment Configuration **Do This:** Ensure that E2E tests run in a dedicated environment that is isolated from the production environment. Use configuration files or environment variables to specify the test environment settings. **Don't Do This:** Run E2E tests against the production environment, which can lead to data corruption and other issues. **Why:** Isolated environment ensures E2E tests don't impact production systems. ## 5. Advanced Testing Techniques ### 5.1. Mutation Testing **Do This:** Use mutation testing tools like Pitest to assess the effectiveness of your unit tests. Mutation testing involves introducing small changes (mutations) to your code and verifying that your tests detect these changes. **Don't Do This:** Assume that your tests are adequate without measuring their effectiveness using mutation testing. **Why:** Mutation testing helps identify gaps in your test suite and ensures that your tests are truly effective in detecting defects. ### 5.2. Contract Testing **Do This:** For microservices, use contract testing tools like Pact to verify that your services correctly adhere to their API contracts. **Don't Do This:** Integrate services without validating the contracts they expose. This can lead to runtime errors and integration issues. **Why:** Contract testing ensures that services can communicate reliably. ### 5.3. Performance Testing **Do This:** Use tools like JMeter, Gatling, or k6 to conduct performance tests and identify bottlenecks in your application. **Don't Do This:** Neglect performance testing, which can lead to slow response times and scalability issues. **Why:** Performance testing helps ensure that your application can handle the expected load and provides a good user experience. ## 6. Continuous Integration (CI) ### 6.1. Automated Builds **Do This:** Integrate your tests into a CI pipeline (e.g., Jenkins, CircleCI, GitHub Actions) to automatically run tests on every code commit. **Don't Do This:** Rely on manual testing, which is error-prone and time-consuming. **Why:** Automated builds with test execution provide early feedback and prevent regressions. ### 6.2. Code Coverage **Do This:** Configure your CI pipeline to collect code coverage metrics and set thresholds to ensure that your tests cover a sufficient percentage of your code. **Don't Do This:** Ignore code coverage metrics, which can lead to untested code and potential defects. **Why:** Code coverage provides valuable insights into the quality of your test suite. ### 6.3. Reporting **Do This:** Configure your CI pipeline to generate test reports and dashboards that provide clear and concise information about test results, code coverage, and other relevant metrics. **Don't Do This:** Neglect to monitor test results and code coverage metrics, which can mask critical issues. **Why:** Test reports and dashboards provide visibility into the health of your application and help identify areas for improvement. By adhering to these testing methodologies standards, Kotlin developers can create high-quality, maintainable, and reliable software that meets the needs of their users and stakeholders.
# API Integration Standards for Kotlin This document outlines the coding standards and best practices for API integration in Kotlin projects. It focuses on patterns for connecting with backend services and external APIs, emphasizing maintainability, performance, and security. All examples are based on the latest Kotlin features. ## 1. Architectural Patterns for API Integration The choice of architectural pattern significantly impacts how APIs are integrated within a Kotlin application. ### 1.1. Using MVVM (Model-View-ViewModel) with Repository Pattern **Standard:** Implement the MVVM pattern in conjunction with a dedicated repository layer for handling data operations, including API calls. The ViewModel should interact solely with the repository, abstracting the data source complexities from the UI. **Why:** This separation of concerns promotes testability, maintainability, and reduces UI coupling with data retrieval logic. **Do This:** """kotlin // Model (Data class) data class User(val id: Int, val name: String, val email: String) // Repository Interface interface UserRepository { suspend fun getUser(id: Int): Result<User> } // Repository Implementation (using Retrofit) class UserRepositoryImpl(private val apiService: ApiService) : UserRepository { override suspend fun getUser(id: Int): Result<User> { return try { val response = apiService.getUser(id) if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(Exception("Error fetching user: ${response.code()}")) } } catch (e: Exception) { Result.failure(e) } } } // ViewModel class UserViewModel(private val userRepository: UserRepository) : ViewModel() { private val _user = MutableLiveData<User>() val user: LiveData<User> = _user private val _error = MutableLiveData<String>() val error: LiveData<String> = _error fun loadUser(userId: Int) { viewModelScope.launch { userRepository.getUser(userId) .onSuccess { _user.value = it } .onFailure { _error.value = it.message } } } } // Retrofit ApiService (example) interface ApiService { @GET("users/{id}") suspend fun getUser(@Path("id") id: Int): Response<User> } """ **Don't Do This:** * Directly making API calls from UI components (Activities, Fragments). * Hardcoding API URLs within the ViewModel. * Omitting error handling for API requests. ### 1.2. Handling API Responses with "Result" **Standard:** Use the "kotlin.Result" type to encapsulate the outcome of API requests and model both success and failure scenarios explicitly. **Why:** "Result" provides a safer and more idiomatic way to manage exceptions compared to traditional "try-catch" approaches. It encourages exhaustive handling of success and failure. **Do This:** """kotlin // Example from the Repository implementation above class UserRepositoryImpl(private val apiService: ApiService) : UserRepository { override suspend fun getUser(id: Int): Result<User> { return try { val response = apiService.getUser(id) if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(Exception("Error fetching user: ${response.code()}")) } } catch (e: Exception) { Result.failure(e) } } } // In the ViewModel, handle Result explicitly class UserViewModel(private val userRepository: UserRepository) : ViewModel() { // ... (LiveData definitions) fun loadUser(userId: Int) { viewModelScope.launch { userRepository.getUser(userId) .onSuccess { _user.value = it } .onFailure { _error.value = it.message } } } } """ **Don't Do This:** * Relying solely on "try-catch" blocks without explicitly addressing both success and failure scenarios. * Ignoring potential exceptions by simply logging them. ## 2. API Client Implementation Selecting and configuring an appropriate HTTP client is critical for reliable API integration. ### 2.1. Using Retrofit and Kotlin Serialization **Standard:** Utilize Retrofit for defining API endpoints as interfaces coupled with Kotlin Serialization or kotlinx.serialization for converting JSON responses to data classes. **Why:** Retrofit simplifies API interaction with type safety, while Kotlin Serialization provides efficient and Kotlin-idiomatic JSON processing (compared to older Java-based libraries). **Do This:** """kotlin // Dependencies (in build.gradle.kts) dependencies { implementation("com.squareup.retrofit2:retrofit:2.17.0") implementation("com.squareup.retrofit2:converter-gson:2.17.0") // GSON converter, alternative is Moshi. // Kotlinx Serialization (Alternative to Gson) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") } kotlin { sourceSets { getByName("commonMain").dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0") } } } // API Interface interface ApiService { @GET("users/{id}") suspend fun getUser(@Path("id") id: Int): Response<User> @POST("users") suspend fun createUser(@Body user: User): Response<User> } @Serializable data class User(val id: Int, val name: String, val email: String) // API Client Initialization val moshi = Moshi.Builder() .addLast(KotlinJsonAdapterFactory()) .build() val client = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY // Log request and response data }) .build() val retrofit = Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(MoshiConverterFactory.create(moshi)) //For moshi .client(client) .build() val apiService = retrofit.create(ApiService::class.java) // Example usage suspend fun main() { try { val response = apiService.getUser(1) if (response.isSuccessful) { val user = response.body() println("User: $user") } else { println("Error: ${response.code()}") } } catch (e: Exception) { println("Exception: ${e.message}") } } """ **Don't Do This:** * Using outdated HTTP clients or serialization libraries. * Failing to configure request logging for debugging. * Manually parsing JSON responses without using a proper serialization mechanism. ### 2.2. Configuring OkHttp Interceptors **Standard:** Use OkHttp interceptors to add common headers (e.g., authentication tokens), log requests/responses, and handle retries. Interceptors should be used for cross-cutting concerns. **Why:** Interceptors provide a centralized and extensible way to modify requests before they are sent and process responses before they reach the application logic. This promotes code reuse and prevents duplication. **Do This:** """kotlin // Authentication Interceptor class AuthInterceptor(private val apiKey: String) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .addHeader("Authorization", "Bearer $apiKey") .build() return chain.proceed(request) } } // Retry Interceptor class RetryInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() var response = chain.proceed(request) var retryCount = 0 while (!response.isSuccessful && retryCount < MAX_RETRIES) { retryCount++ Thread.sleep(RETRY_DELAY) // Avoid immediate retries response.close() // Ensure previous response is closed println("Retrying request: $retryCount") response = chain.proceed(request) } return response } companion object { private const val MAX_RETRIES = 3 private const val RETRY_DELAY = 1000L // 1 second } } // Update OkHttpClient builder in the API initialization section to include the custom interceptors. val client = OkHttpClient.Builder() .addInterceptor(AuthInterceptor("YOUR_API_KEY")) .addInterceptor(RetryInterceptor()) .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .build() """ **Don't Do This:** * Implementing interceptor logic directly within the API service definition. * Performing complex computations or I/O operations within interceptors (keep them lightweight). * Retrying requests indefinitely without a maximum retry count. ## 3. Data Handling and Serialization Efficiently handling data transferred from APIs is crucial for application performance and maintainability. Focus on modern, idiomatic approaches. ### 3.1. Using "kotlinx.serialization" for Data Classes **Standard:** For Kotlin projects, prefer "kotlinx.serialization" over older JSON parsing libraries. For Java interoperability or specific library requirements, Gson or Jackson can be used, but Kotlin serialization provides a best-in-Kotlin solution. **Why:** "kotlinx.serialization" is designed specifically for Kotlin data classes and integrates seamlessly with the language. It reduces boilerplate and improves performance. **Do This:** """kotlin import kotlinx.serialization.* import kotlinx.serialization.json.* @Serializable data class Article( val id: Int, val title: String, val body: String, @SerialName("published_date") // Specify custom name for JSON field val publishedDate: String ) fun main() { val jsonString = """ { "id": 123, "title": "Kotlin Serialization", "body": "Example of kotlinx.serialization", "published_date": "2024-06-22" } """ val article: Article = Json.decodeFromString(jsonString) println(article) val jsonOutput = Json.encodeToString(article) println(jsonOutput) } """ **Don't Do This:** * Using reflection-based serialization libraries with Kotlin data classes if "kotlinx.serialization" is a viable option. * Writing manual JSON parsing logic. * Ignoring transient or computed properties during serialization/deserialization. ### 3.2. Handling Nullable Fields **Standard:** Leverage Kotlin's null safety features when defining data classes and models representing API responses. Clearly define which fields are nullable and handle them appropriately. **Why:** Prevents NullPointerExceptions and enforces explicitness regarding the possibility of missing data. **Do This:** """kotlin @Serializable data class UserProfile( val id: Int, val name: String?, // Name can be null val email: String ) fun processUserProfile(profile: UserProfile) { val displayName = profile.name ?: "Unknown User" // Handle null name with elvis operator println("Display Name: $displayName") } """ **Don't Do This:** * Forcing every field in a data class to be non-nullable if the API might return null values for those fields. * Ignoring potentially null fields without proper handling. ## 4. Error Handling and Resilience Building robust error handling mechanisms is essential for providing a stable user experience even when APIs fail. ### 4.1. Implementing Retry Policies **Standard:** Implement retry policies with exponential backoff for transient API errors like network glitches or server overload. **Why:** Increases resilience against temporary failures without overwhelming the API. **Do This:** """kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking suspend fun <T> retry( times: Int = 3, initialDelay: Long = 100, // milliseconds maxDelay: Long = 1000, factor: Double = 2.0, block: suspend () -> T ): T { var currentDelay = initialDelay repeat(times) { try { return block() } catch (e: Exception) { if (it == times - 1) throw e // Last attempt, rethrow exception println("Retrying after delay: $currentDelay ms") delay(currentDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } } throw IllegalStateException("Retry failed") // Should not happen } // Usage example fun main() = runBlocking { try { val result = retry { makeApiCall() } println("Result: $result") } catch (e: Exception) { println("API call failed after retries: ${e.message}") } } suspend fun makeApiCall(): String { // Simulate API call with occasional failures if ((0..2).random() == 0) { throw RuntimeException("API temporarily unavailable") } return "API Response" } """ **Don't Do This:** * Retrying non-transient errors (e.g., authentication failures) without addressing the underlying issue. * Implementing a fixed delay without exponential backoff, which can overload the API during widespread outages. ### 4.2. Using "Either" or Sealed Classes for Error Representation **Standard:** Consider using "Either" or sealed classes to represent API call outcomes where an API is not using HTTP codes appropriately. **Why:** Provides a strongly-typed way to handle different error scenarios. **Do This:** """kotlin // Using a sealed class sealed class ApiResult<out T : Any> { data class Success<out T : Any>(val data: T) : ApiResult<T>() data class Error(val exception: Exception) : ApiResult<Nothing>() } // API Call fun getUserData(): ApiResult<UserData> { return try { val data = // Some kind of data fetched from API ApiResult.Success(data) } catch (e:Exception) { ApiResult.Error(e) } } //Consuming the result val apiResult = getUserData() when (apiResult) { is ApiResult.Success -> { apiResult.data // Your successful data } is ApiResult.Error -> { apiResult.exception // The exception thrown } } """ **Don't Do This:** * Using generic exception handling without distinguishing between different error types. * Relying solely on HTTP status codes for comprehensive error reporting. ## 5. Security Considerations API integration introduces potential security vulnerabilities. Addressing them proactively is critical. ### 5.1. Securely Storing API Keys **Standard:** Never store API keys directly in code. Use environment variables, secure configuration files, or dedicated secrets management solutions. **Why:** Prevents accidental exposure of sensitive credentials in version control and other environments. Ideally retrieve from Hashicorp Vault or comparable platforms. **Do This:** * Store API keys in environment variables. * Use Android's "BuildConfig" fields (for Android apps) populated from Gradle. * Employ a secrets management service (e.g., AWS Secrets Manager, Google Cloud Secret Manager). **Don't Do This:** * Hardcoding API keys in source code. * Committing configuration files containing API keys to version control. * Exposing API keys in client-side code (JavaScript, etc.). All API key usage should occur on backend/server code. ### 5.2. Validating API Responses **Standard:** Validate API responses to ensure data integrity and prevent injection attacks. **Why:** Protects against malicious or corrupted data returned by the API. **Do This:** * Verify data types and formats. * Sanitize input fields. * Implement rate limiting to prevent abuse. **Don't Do This:** * Trusting API responses blindly. * Omitting input validation, especially for user-provided data. * Exposing sensitive data in API responses unnecessarily. Sensitive information should only be available with authorization. ## 6. Performance Optimization Optimizing API integration for performance is essential for providing a responsive user experience. ### 6.1. Caching API Responses **Standard:** Cache API responses to reduce network traffic and improve response times. Use both in-memory and persistent caching mechanisms. **Why:** Minimizes redundant API calls and reduces latency. **Do This:** * Implement HTTP caching using "Cache-Control" headers. * Use a caching library like "kotlin-faker" or "Kache". * Cache data in memory for frequently accessed resources. * Persist data to disk or a database for long-term storage. **Don't Do This:** * Caching sensitive data without proper encryption. * Using overly aggressive caching that leads to stale data. * Failing to invalidate the cache when data changes. ### 6.2. Gzip Compression **Standard:** Enable Gzip compression for API requests and responses to reduce network bandwidth usage. **Why:** Reduces the size of data transferred over the network, improving performance, especially on slow connections. **Do This:** * Configure OkHttp to automatically compress requests. * Ensure the API server supports Gzip compression. **Don't Do This:** * Omitting compression for large API payloads. ## Conclusion Adhering to these coding standards and best practices will lead to more robust, secure, and maintainable Kotlin applications that seamlessly integrate with external APIs. These guidelines are intended to be a "living document" and should be regularly reviewed and updated to reflect the latest Kotlin features and industry best practices.