Chuleta mental para no escribir Dart con sintaxis Kotlin. No es una traducción literal: es un cambio de modelo.
1. Cambios de mentalidad
| Flutter/Dart | Kotlin/Android | Qué cambia de verdad |
|---|---|---|
pubspec.yaml | build.gradle.kts | Gradle es un sistema de build con plugins y tareas, no un gestor de paquetes. |
package:foo | módulo Gradle :feature:foo | Los módulos separan compilación, dependencias y límites de API. |
BuildContext | dependencias explícitas | No hay un contexto global del árbol de widgets. |
setState | UiState + StateFlow | El estado sube; la UI observa. |
ChangeNotifier/Provider/Riverpod | ViewModel + Hilt | El ViewModel sobrevive a configuración; Hilt construye el grafo. |
Future<T> | suspend fun | La cancelación y el scope son parte del diseño. |
Stream<T> | Flow<T> | Flow es frío por defecto; StateFlow es caliente y tiene valor. |
Widget | @Composable | No hay árbol de widgets con build; hay recomposición. |
copyWith generado por freezed | data class.copy() | copy() es superficial. |
== de valor con freezed | data class | equals/hashCode se generan. |
dynamic | Any/genéricos | Kotlin es estático; evita Any como comodín. |
2. Variables y tipos
final String name = 'Alex';
var retries = 3;
const double rate = 0.21;
val name: String = "Alex"
var retries = 3
const val RATE = 0.21
val≈final. No reasigna la referencia; el objeto puede ser mutable.var≈var.const val≈const, solo para constantes de compilación.El tipo va después del nombre:
name: String.La inferencia existe:
val retries = 3esInt.
Tipos básicos:
| Dart | Kotlin |
|---|---|
int | Int, Long |
double | Double |
bool | Boolean |
String | String |
List<T> | List<T> / MutableList<T> |
Map<K, V> | Map<K, V> / MutableMap<K, V> |
Set<T> | Set<T> / MutableSet<T> |
Object? | Any? |
void | Unit |
Never | Nothing |
Trampas de números viniendo de Dart:
El
intde Dart es de 64 bits; elIntde Kotlin es de 32 bits (máx. ~2.147 millones). Para céntimos, IDs o timestamps usaLong(0L,100_000L).No hay conversión implícita:
val l: Long = unIntno compila. UsaunInt.toLong()./entre enteros es división entera (el~/de Dart). Para decimales, uno de los dos debe serDouble.==compara valor (llama aequals);===compara referencia (elidentical()de Dart).
3. Null safety
String? nickname;
final shown = nickname ?? 'sin alias';
final length = nickname?.length ?? 0;
val nickname: String? = null
val shown = nickname ?: "sin alias"
val length = nickname?.length ?: 0
String?es un tipo distinto deString.?.y?:son iguales que en Dart.!!existe, pero es una promesa al compilador. Si falla, crash.requireNotNull(x)expresa un contrato;checkNotNull(x)comprueba estado.En Kotlin, una API que declara
Stringno puede devolvernull.
4. Funciones
String greet(String name) {
return 'Hola, $name';
}
int double(int value) => value * 2;
fun greet(name: String): String {
return "Hola, $name"
}
fun double(value: Int): Int = value * 2
fundelante.Parámetros:
name: String.Retorno después de los paréntesis.
Unitse omite.Argumentos con nombre y valores por defecto:
fun transfer(amountCents: Long, reference: String = ""): String =
"Transferencia de $amountCents céntimos ($reference)"
5. Clases, data class y sealed
class Account {
final String id;
final String name;
final int balanceCents;
const Account({
required this.id,
required this.name,
required this.balanceCents,
});
Account copyWith({String? name}) => Account(
id: id,
name: name ?? this.name,
balanceCents: balanceCents,
);
}
data class Account(
val id: String,
val name: String,
val balanceCents: Long,
)
val updated = account.copy(name = "Reserve")
data class genera equals, hashCode, toString, copy y componentN.
Estados:
enum TransferState { idle, loading, success, error }
En Kotlin, si cada estado lleva datos distintos, usa sealed:
sealed interface TransferState {
data object Idle : TransferState
data object Loading : TransferState
data class Success(val receiptId: String) : TransferState
data class Error(val message: String) : TransferState
}
Ventaja: el when es exhaustivo y el compilador te obliga a tratar todos los casos.
6. Colecciones y lambdas
final names = users.where((u) => u.active).map((u) => u.name).toList();
final total = transactions.fold(0, (sum, t) => sum + t.amountCents);
val names = users.filter { it.active }.map { it.name }
val total = transactions.sumOf { it.amountCents }
ites el parámetro implícito de una lambda de un argumento.filter≈where;map≈map;fold≈fold.sumOf,groupBy,associateBy,sortedBy,partitionson muy usadas.Listes de solo lectura por interfaz;MutableListpermite mutar.listOf()es inmutable;mutableListOf()es mutable.
Scope functions:
| Función | Uso típico |
|---|---|
let | transformar o hacer algo con un nullable |
run | bloque con receptor y resultado |
with | varias operaciones sobre un objeto |
apply | configurar un objeto y devolverlo |
also | efecto lateral y devolver el mismo objeto |
No las uses para parecer listo. Si un if normal se lee mejor, usa el if.
7. Async: Future vs suspend
Future<Account> loadAccount(String id) async {
await Future.delayed(const Duration(milliseconds: 100));
return Account(id: id, name: 'Business Giro');
}
suspend fun loadAccount(id: String): Account {
delay(100)
return Account(id, "Business Giro")
}
suspendno bloquea el hilo; el compilador transforma la función.Se llama desde una coroutine o desde otra función
suspend.launchpara fuego y olvido;async/awaitpara resultado.withContext(Dispatchers.IO)para trabajo de bloqueo.La cancelación es cooperativa: las funciones
suspenddeben comprobarisActiveo usar APIs cancelables.
8. Streams vs Flow
Stream<List<Transaction>> watchTransactions() { ... }
fun watchTransactions(): Flow<List<Transaction>> = flow {
emit(emptyList())
emit(repository.load())
}
Flowes frío: cada colector ejecuta el bloque.StateFlowes caliente, tienevaluey siempre emite el último estado.SharedFlowes caliente y puede emitir eventos sin valor inicial.collectAsStateWithLifecycle()en Compose respeta el ciclo de vida.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initial)es el patrón habitual para estado.
9. Compose vs Flutter
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('Hola, $name');
}
}
@Composable
fun Greeting(name: String) {
Text("Hola, $name")
}
Un
@Composablees una función, no una clase.No hay
build(); Compose decide qué recomponer.rememberguarda estado entre recomposiciones;rememberSaveablelo guarda ante muerte de proceso.LaunchedEffect(key)ejecuta efectos suspendidos.El estado sube:
value+onValueChange.No hay hot reload como en Flutter:
@Previewte deja ver estados sin ejecutar la app y Live Edit actualiza composables en caliente, con limitaciones.
Equivalencias:
| Flutter | Compose |
|---|---|
StatelessWidget | @Composable sin estado |
StatefulWidget + setState | @Composable + remember + state hoisting |
ListView.builder | LazyColumn |
Column/Row | Column/Row |
Padding | Modifier.padding |
Theme.of(context) | MaterialTheme |
Navigator | Navigation Compose |
Provider/Riverpod | ViewModel + StateFlow + Hilt |
Para profundizar en layout, estado, listas, efectos, navegación y tests, lee docs/COMPOSE.md.
10. Arquitectura y DI
| Flutter | Android/Kotlin |
|---|---|
ChangeNotifier | ViewModel |
Provider/Riverpod | Hilt + ViewModel |
get_it | Hilt |
Repository manual | interface + @Binds/@Provides |
freezed | data class + sealed |
json_serializable | kotlinx.serialization |
dio/http | Retrofit/OkHttp |
sqflite/Drift | Room |
shared_preferences | DataStore |
flutter_secure_storage | Android Keystore + Tink (EncryptedSharedPreferences está deprecado) |
11. Testing
| Flutter | Kotlin/Android |
|---|---|
test package | JUnit 5 / kotlin.test |
flutter_test | Compose UI Test |
mocktail/mockito | fakes manuales, MockK o Mockito |
Stream test | Turbine + runTest |
| golden tests | screenshot tests (Paparazzi/Roborazzi) |
Empieza con fakes manuales. Un repositorio en memoria suele ser más claro que un mock.
12. Build y proyecto
pubspec.yaml→build.gradle.kts+settings.gradle.kts.flutter pub get→ Gradle resuelve dependencias al compilar.flutter run→./gradlew installDebugo Run en Android Studio.flutter test→./gradlew test.flutter analyze→./gradlew lint(Android) y el compilador de Kotlin.flutter build apk→./gradlew assembleDebug/assembleRelease.
13. Lo que no debes traducir
No traduzcas
BuildContexta un parámetro global. Pasa dependencias.No uses
Anydonde Dart usaríadynamic. Modela el tipo.No hagas
data classconvarpor comodidad. El estado inmutable evita bugs.No uses
!!para callar al compilador.No conviertas todo en
Flow; una funciónsuspendque devuelve un valor es suficiente.No hagas
rememberde algo que debe sobrevivir a la navegación; usaViewModel.No metas lógica de negocio en un
@Composable.No portes la estructura de carpetas de Flutter tal cual. Piensa en módulos y límites.
14. Ejercicio de traducción
Traduce este Dart a Kotlin, con tests:
class Transaction {
final String id;
final String counterparty;
final int amountCents;
const Transaction(this.id, this.counterparty, this.amountCents);
}
List<Transaction> income(List<Transaction> items) =>
items.where((t) => t.amountCents > 0).toList();
int total(List<Transaction> items) =>
items.fold(0, (sum, t) => sum + t.amountCents);
Solución esperada en Kotlin:
data class Transaction(
val id: String,
val counterparty: String,
val amountCents: Long,
)
fun income(items: List<Transaction>): List<Transaction> =
items.filter { it.amountCents > 0 }
fun total(items: List<Transaction>): Long =
items.sumOf { it.amountCents }
Si te sale así, ya estás pensando en Kotlin.