Padrões de Desenvolvimento Kotlin
Padrões idiomáticos Kotlin e melhores práticas para construir aplicações robustas, eficientes e de fácil manutenção.
Quando Usar
- Escrevendo novo código Kotlin
- Revisando código Kotlin
- Refatorando código Kotlin existente
- Projetando módulos ou bibliotecas Kotlin
- Configurando builds com Gradle Kotlin DSL
Como Funciona
Este skill impõe convenções idiomáticas Kotlin em sete áreas principais: segurança de nulos usando o sistema de tipos e operadores de chamada segura, imutabilidade via val e copy() em data classes, classes e interfaces seladas para hierarquias de tipos exaustivas, concorrência estruturada com corrotinas e Flow, funções de extensão para adicionar comportamento sem herança, construtores DSL type-safe usando @DslMarker e lambdas com receptor, e Gradle Kotlin DSL para configuração de build.
Exemplos
Segurança de nulos com operador Elvis:
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
Classe selada para resultados exaustivos:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
Concorrência estruturada com async/await:
suspend fun fetchUserWithPosts(userId: String): UserProfile =
coroutineScope {
val user = async { userService.getUser(userId) }
val posts = async { postService.getUserPosts(userId) }
UserProfile(user = user.await(), posts = posts.await())
}
Princípios Fundamentais
1. Segurança de Nulos
O sistema de tipos do Kotlin distingue tipos anuláveis e não anuláveis. Aproveite-o ao máximo.
// Bom: Use tipos não anuláveis por padrão
fun getUser(id: String): User {
return userRepository.findById(id)
?: throw UserNotFoundException("User $id not found")
}
// Bom: Chamadas seguras e operador Elvis
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
// Ruim: Forçar desembrulho de tipos anuláveis
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email // Lança NPE se for nulo
}
2. Imutabilidade por Padrão
Prefira val sobre var, coleções imutáveis sobre mutáveis.
// Bom: Dados imutáveis
data class User(
val id: String,
val name: String,
val email: String,
)
// Bom: Transformar com copy()
fun updateEmail(user: User, newEmail: String): User =
user.copy(email = newEmail)
// Bom: Coleções imutáveis
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }
// Ruim: Estado mutável
var currentUser: User? = null // Evite estado global mutável
val mutableUsers = mutableListOf<User>() // Evite a menos que realmente necessário
3. Corpos de Expressão e Funções de Expressão Única
Use corpos de expressão para funções concisas e legíveis.
// Bom: Corpo de expressão
fun isAdult(age: Int): Boolean = age >= 18
fun formatFullName(first: String, last: String): String =
"$first $last".trim()
fun User.displayName(): String =
name.ifBlank { email.substringBefore('@') }
// Bom: When como expressão
fun statusMessage(code: Int): String = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Internal Server Error"
else -> "Unknown status: $code"
}
// Ruim: Corpo de bloco desnecessário
fun isAdult(age: Int): Boolean {
return age >= 18
}
4. Data Classes para Objetos de Valor
Use data classes para tipos que primariamente armazenam dados.
// Bom: Data class com copy, equals, hashCode, toString
data class CreateUserRequest(
val name: String,
val email: String,
val role: Role = Role.USER,
)
// Bom: Value class para segurança de tipos (zero overhead em tempo de execução)
@JvmInline
value class UserId(val value: String) {
init {
require(value.isNotBlank()) { "UserId cannot be blank" }
}
}
@JvmInline
value class Email(val value: String) {
init {
require('@' in value) { "Invalid email: $value" }
}
}
fun getUser(id: UserId): User = userRepository.findById(id)
Classes e Interfaces Seladas
Modelando Hierarquias Restritas
// Bom: Classe selada para when exaustivo
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
fun <T> Result<T>.getOrNull(): T? = when (this) {
is Result.Success -> data
is Result.Failure -> null
is Result.Loading -> null
}
fun <T> Result<T>.getOrThrow(): T = when (this) {
is Result.Success -> data
is Result.Failure -> throw error.toException()
is Result.Loading -> throw IllegalStateException("Still loading")
}
Interfaces Seladas para Respostas de API
sealed interface ApiError {
val message: String
data class NotFound(override val message: String) : ApiError
data class Unauthorized(override val message: String) : ApiError
data class Validation(
override val message: String,
val field: String,
) : ApiError
data class Internal(
override val message: String,
val cause: Throwable? = null,
) : ApiError
}
fun ApiError.toStatusCode(): Int = when (this) {
is ApiError.NotFound -> 404
is ApiError.Unauthorized -> 401
is ApiError.Validation -> 422
is ApiError.Internal -> 500
}
Funções de Escopo
Quando Usar Cada Uma
// let: Transformar resultado anulável ou com escopo
val length: Int? = name?.let { it.trim().length }
// apply: Configurar um objeto (retorna o objeto)
val user = User().apply {
name = "Alice"
email = "[email protected]"
}
// also: Efeitos colaterais (retorna o objeto)
val user = createUser(request).also { logger.info("Created user: ${it.id}") }
// run: Executar um bloco com receptor (retorna resultado)
val result = connection.run {
prepareStatement(sql)
executeQuery()
}
// with: Forma não extensão de run
val csv = with(StringBuilder()) {
appendLine("name,email")
users.forEach { appendLine("${it.name},${it.email}") }
toString()
}
Anti-Padrões
// Ruim: Aninhar funções de escopo
user?.let { u ->
u.address?.let { addr ->
addr.city?.let { city ->
println(city) // Difícil de ler
}
}
}
// Bom: Encadear chamadas seguras
val city = user?.address?.city
city?.let { println(it) }
Funções de Extensão
Adicionando Funcionalidade Sem Herança
// Bom: Extensões específicas de domínio
fun String.toSlug(): String =
lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
fun Instant.toLocalDate(zone: ZoneId = ZoneId.systemDefault()): LocalDate =
atZone(zone).toLocalDate()
// Bom: Extensões de coleção
fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// Bom: Extensões com escopo (não poluem o namespace global)
class UserService {
private fun User.isActive(): Boolean =
status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))
fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
}
Corrotinas
Concorrência Estruturada
// Bom: Concorrência estruturada com coroutineScope
suspend fun fetchUserWithPosts(userId: String): UserProfile =
coroutineScope {
val userDeferred = async { userService.getUser(userId) }
val postsDeferred = async { postService.getUserPosts(userId) }
UserProfile(
user = userDeferred.await(),
posts = postsDeferred.await(),
)
}
// Bom: supervisorScope quando filhos podem falhar independentemente
suspend fun fetchDashboard(userId: String): Dashboard =
supervisorScope {
val user = async { userService.getUser(userId) }
val notifications = async { notificationService.getRecent(userId) }
val recommendations = async { recommendationService.getFor(userId) }
Dashboard(
user = user.await(),
notifications = try {
notifications.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emptyList()
},
recommendations = try {
recommendations.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emptyList()
},
)
}
Flow para Streams Reativos
// Bom: Flow frio com tratamento de erro adequado
fun observeUsers(): Flow<List<User>> = flow {
while (currentCoroutineContext().isActive) {
val users = userRepository.findAll()
emit(users)
delay(5.seconds)
}
}.catch { e ->
logger.error("Error observing users", e)
emit(emptyList())
}
// Bom: Operadores Flow
fun searchUsers(query: Flow<String>): Flow<List<User>> =
query
.debounce(300.milliseconds)
.distinctUntilChanged()
.filter { it.length >= 2 }
.mapLatest { q -> userRepository.search(q) }
.catch { emit(emptyList()) }
Cancelamento e Limpeza
// Bom: Respeitar cancelamento
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
ensureActive() // Verificar cancelamento antes de trabalho pesado
processItem(item)
}
}
// Bom: Limpeza com try/finally
suspend fun acquireAndProcess() {
val resource = acquireResource()
try {
resource.process()
} finally {
withContext(NonCancellable) {
resource.release() // Sempre liberar, mesmo em cancelamento
}
}
}
Delegação
Delegação de Propriedades
// Inicialização preguiçosa
val expensiveData: List<User> by lazy {
userRepository.findAll()
}
// Propriedade observável
var name: String by Delegates.observable("initial") { _, old, new ->
logger.info("Name changed from '$old' to '$new'")
}
// Propriedades baseadas em mapa
class Config(private val map: Map<String, Any?>) {
val host: String by map
val port: Int by map
val debug: Boolean by map
}
val config = Config(mapOf("host" to "localhost", "port" to 8080, "debug" to true))
Delegação de Interface
// Bom: Delegar implementação de interface
class LoggingUserRepository(
private val delegate: UserRepository,
private val logger: Logger,
) : UserRepository by delegate {
// Sobrescreva apenas o que precisa para adicionar log
override suspend fun findById(id: String): User? {
logger.info("Finding user by id: $id")
return delegate.findById(id).also {
logger.info("Found user: ${it?.name ?: "null"}")
}
}
}
Construtores DSL
Construtores Type-Safe
// Bom: DSL com @DslMarker
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML {
private val children = mutableListOf<Element>()
fun head(init: Head.() -> Unit) {
children += Head().apply(init)
}
fun body(init: Body.() -> Unit) {
children += Body().apply(init)
}
override fun toString(): String = children.joinToString("\n")
}
fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)
// Uso
val page = html {
head { title("My Page") }
body {
h1("Welcome")
p("Hello, World!")
}
}
DSL de Configuração
data class ServerConfig(
val host: String = "0.0.0.0",
val port: Int = 8080,
val ssl: SslConfig? = null,
val database: DatabaseConfig? = null,
)
data class SslConfig(val certPath: String, val keyPath: String)
data class DatabaseConfig(val url: String, val maxPoolSize: Int = 10)
class ServerConfigBuilder {
var host: String = "0.0.0.0"
var port: Int = 8080
private var ssl: SslConfig? = null
private var database: DatabaseConfig? = null
fun ssl(certPath: String, keyPath: String) {
ssl = SslConfig(certPath, keyPath)
}
fun database(url: String, maxPoolSize: Int = 10) {
database = DatabaseConfig(url, maxPoolSize)
}
fun build(): ServerConfig = ServerConfig(host, port, ssl, database)
}
fun serverConfig(init: ServerConfigBuilder.() -> Unit): ServerConfig =
ServerConfigBuilder().apply(init).build()
// Uso
val config = serverConfig {
host = "0.0.0.0"
port = 443
ssl("/certs/cert.pem", "/certs/key.pem")
database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)
}
Sequências para Avaliação Preguiçosa
// Bom: Use sequências para coleções grandes com múltiplas operações
val result = users.asSequence()
.filter { it.isActive }
.map { it.email }
.filter { it.endsWith("@company.com") }
.take(10)
.toList()
// Bom: Gerar sequências infinitas
val fibonacci: Sequence<Long> = sequence {
var a = 0L
var b = 1L
while (true) {
yield(a)
val next = a + b
a = b
b = next
}
}
val first20 = fibonacci.take(20).toList()
Gradle Kotlin DSL
Configuração build.gradle.kts
// Verifique as versões mais recentes: https://kotlinlang.org/docs/releases.html
plugins {
kotlin("jvm") version "2.3.10"
kotlin("plugin.serialization") version "2.3.10"
id("io.ktor.plugin") version "3.4.0"
id("org.jetbrains.kotlinx.kover") version "0.9.7"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
group = "com.example"
version = "1.0.0"
kotlin {
jvmToolchain(21)
}
dependencies {
// Ktor
implementation("io.ktor:ktor-server-core:3.4.0")
implementation("io.ktor:ktor-server-netty:3.4.0")
implementation("io.ktor:ktor-server-content-negotiation:3.4.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.0")
// Exposed
implementation("org.jetbrains.exposed:exposed-core:1.
...
[truncado para tradução]
Considerações de Performance
Alocações e Uso de Memória
// Bom: Sequências para evitar coleções intermediárias
val heavyResult = largeList.asSequence()
.filter { expensivePredicate(it) }
.map { transform(it) }
.toList()
// Bom: Usar lazy ao invés de eager com sequences
fun findFirstMatch(items: List<Item>): Item? =
items.asSequence()
.mapNotNull { item ->
val match = expensiveComputation(item)
match.takeIf { it.isValid() }
}
.firstOrNull()
// Ruim: Múltiplas transformações eager
val result = largeList
.filter { predicate(it) } // Aloca lista A
.map { transform(it) } // Aloca lista B
.take(10) // Aloca lista C
Otimizações de String
// Bom: Usar StringBuilder para concatenações múltiplas
val builder = StringBuilder()
(1..1000).forEach { builder.append("Line $it\n") }
val result = builder.toString()
// Ou melhor: Usar buildString
val result = buildString {
(1..1000).forEach { append("Line $it\n") }
}
// Bom: Interpolação de template única
val userInfo = "User: ${user.name}, Email: ${user.email}"
// Ruim: Múltiplas concatenações
var result = ""
(1..1000).forEach { result += "Line $it\n" } // O(n²) no tempo
Convenções Gerais
Estrutura de Arquivos
// Bom: Um arquivo por classe (classes públicas)
// User.kt
package com.example.model
data class User(
val id: String,
val name: String,
val email: String,
)
// Bom: Classes privadas ou estreitamente relacionadas em um arquivo
// result.kt
sealed class Result<out T>
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
// Bom: Funções de extensão no final do arquivo
fun User.displayString(): String = "<${email}> $name"
Nomenclatura
// Classes: PascalCase
class UserRepository
class UserService
class UserController
// Funções e propriedades: camelCase
fun processUser()
val userName: String
// Constantes: SCREAMING_SNAKE_CASE
const val MAX_RETRIES = 3
val DEFAULT_TIMEOUT_MS = 5000L
// Tipos genéricos: T, U, V ou nomes descritivos
class Result<out T>
class Mapper<in Input, out Output>
interface Repository<E, ID>
Testes
Escrevendo Testes Legíveis
// Bom: Use nomes descritivos para funções de teste e blocos
@Test
fun `should calculate total price with discount`() {
// Arrange (Organizar)
val items = listOf(Item(price = 100.0), Item(price = 200.0))
val discount = 0.1
// Act (Agir)
val total = calculateTotal(items, discount)
// Assert (Afirmar)
assertEquals(270.0, total, 0.001)
}
// Bom: Use `should` ou `shouldBe` em testes de Kotest
class PriceCalculatorTest : StringSpec({
"should calculate total for empty list" {
calculateTotal(emptyList(), 0.0) shouldBe 0.0
}
"should apply discount correctly" {
val items = listOf(Item(price = 100.0), Item(price = 200.0))
calculateTotal(items, 0.1) shouldBe 270.0
}
})
Contextos de Uso Comuns
Serviços REST com Ktor
fun Application.module() {
install(ContentNegotiation) {
json()
}
routing {
get("/users/{id}") {
val id = call.parameters["id"] ?: throw BadRequestException()
val user = userService.findById(id)
call.respond(user)
}
}
}
Repositórios com Exposed
data class UserEntity(id: String, name: String, email: String)
object Users : Table() {
val id = varchar("id", 36) // UUID
val name = varchar("name", 255)
val email = varchar("email", 255)
override val primaryKey = PrimaryKey(id)
}
class UserRepository(private val db: Database) {
suspend fun findById(id: String): UserEntity? = dbQuery {
Users.selectAll().where { Users.id eq id }
.map { it.toUserEntity() }
.singleOrNull()
}
}
Estado com StateFlow
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = userRepository.findById(id)
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message ?: "Unknown error")
}
}
}
}