Communitygithub.com

Padrões Kotlin

Padrões idiomáticos Kotlin, melhores práticas e convenções para construir aplicações Kotlin robustas, eficientes e de fácil manutenção com corrotinas, segurança de nulos e construtores DSL. Use ao escrever ou revisar código Kotlin e quando a estrutura idiomática ou segurança de nulos estiver em questão.

O que é Padrões Kotlin?

Este skill estabelece um conjunto abrangente de padrões e melhores práticas para o desenvolvimento idiomático em Kotlin. Ele cobre sete áreas principais: segurança de nulos através do 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. O objetivo é ajudar desenvolvedores a escrever código Kotlin mais limpo, seguro e expressivo, que aproveite ao máximo as características da linguagem. O skill também oferece exemplos práticos e anti-padrões para cada área, garantindo que as convenções sejam aplicadas corretamente em projetos Kotlin de qualquer escala. É suportado em todas as plataformas que executam Kotlin, incluindo JVM, Android, Native e JS.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/kotlin-patterns

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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")
            }
        }
    }
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

Habilidades Relacionadas