Kotlin Lab

Aprender / Flutter/Dart → Kotlin/Android

Flutter/Dart → Kotlin/Android

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/DartKotlin/AndroidQué cambia de verdad
pubspec.yamlbuild.gradle.ktsGradle es un sistema de build con plugins y tareas, no un gestor de paquetes.
package:foomódulo Gradle :feature:fooLos módulos separan compilación, dependencias y límites de API.
BuildContextdependencias explícitasNo hay un contexto global del árbol de widgets.
setStateUiState + StateFlowEl estado sube; la UI observa.
ChangeNotifier/Provider/RiverpodViewModel + HiltEl ViewModel sobrevive a configuración; Hilt construye el grafo.
Future<T>suspend funLa 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@ComposableNo hay árbol de widgets con build; hay recomposición.
copyWith generado por freezeddata class.copy()copy() es superficial.
== de valor con freezeddata classequals/hashCode se generan.
dynamicAny/genéricosKotlin 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 = 3 es Int.

Tipos básicos:

DartKotlin
intInt, Long
doubleDouble
boolBoolean
StringString
List<T>List<T> / MutableList<T>
Map<K, V>Map<K, V> / MutableMap<K, V>
Set<T>Set<T> / MutableSet<T>
Object?Any?
voidUnit
NeverNothing

Trampas de números viniendo de Dart:

  • El int de Dart es de 64 bits; el Int de Kotlin es de 32 bits (máx. ~2.147 millones). Para céntimos, IDs o timestamps usa Long (0L, 100_000L).

  • No hay conversión implícita: val l: Long = unInt no compila. Usa unInt.toLong().

  • / entre enteros es división entera (el ~/ de Dart). Para decimales, uno de los dos debe ser Double.

  • == compara valor (llama a equals); === compara referencia (el identical() 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 de String.

  • ?. 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 String no puede devolver null.

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
  • fun delante.

  • Parámetros: name: String.

  • Retorno después de los paréntesis.

  • Unit se 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 }
  • it es el parámetro implícito de una lambda de un argumento.

  • filter ≈ where; map ≈ map; fold ≈ fold.

  • sumOf, groupBy, associateBy, sortedBy, partition son muy usadas.

  • List es de solo lectura por interfaz; MutableList permite mutar.

  • listOf() es inmutable; mutableListOf() es mutable.

Scope functions:

FunciónUso típico
lettransformar o hacer algo con un nullable
runbloque con receptor y resultado
withvarias operaciones sobre un objeto
applyconfigurar un objeto y devolverlo
alsoefecto 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")
}
  • suspend no bloquea el hilo; el compilador transforma la función.

  • Se llama desde una coroutine o desde otra función suspend.

  • launch para fuego y olvido; async/await para resultado.

  • withContext(Dispatchers.IO) para trabajo de bloqueo.

  • La cancelación es cooperativa: las funciones suspend deben comprobar isActive o usar APIs cancelables.

8. Streams vs Flow

Stream<List<Transaction>> watchTransactions() { ... }
fun watchTransactions(): Flow<List<Transaction>> = flow {
    emit(emptyList())
    emit(repository.load())
}
  • Flow es frío: cada colector ejecuta el bloque.

  • StateFlow es caliente, tiene value y siempre emite el último estado.

  • SharedFlow es 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 @Composable es una función, no una clase.

  • No hay build(); Compose decide qué recomponer.

  • remember guarda estado entre recomposiciones; rememberSaveable lo guarda ante muerte de proceso.

  • LaunchedEffect(key) ejecuta efectos suspendidos.

  • El estado sube: value + onValueChange.

  • No hay hot reload como en Flutter: @Preview te deja ver estados sin ejecutar la app y Live Edit actualiza composables en caliente, con limitaciones.

Equivalencias:

FlutterCompose
StatelessWidget@Composable sin estado
StatefulWidget + setState@Composable + remember + state hoisting
ListView.builderLazyColumn
Column/RowColumn/Row
PaddingModifier.padding
Theme.of(context)MaterialTheme
NavigatorNavigation Compose
Provider/RiverpodViewModel + StateFlow + Hilt

Para profundizar en layout, estado, listas, efectos, navegación y tests, lee docs/COMPOSE.md.

10. Arquitectura y DI

FlutterAndroid/Kotlin
ChangeNotifierViewModel
Provider/RiverpodHilt + ViewModel
get_itHilt
Repository manualinterface + @Binds/@Provides
freezeddata class + sealed
json_serializablekotlinx.serialization
dio/httpRetrofit/OkHttp
sqflite/DriftRoom
shared_preferencesDataStore
flutter_secure_storageAndroid Keystore + Tink (EncryptedSharedPreferences está deprecado)

11. Testing

FlutterKotlin/Android
test packageJUnit 5 / kotlin.test
flutter_testCompose UI Test
mocktail/mockitofakes manuales, MockK o Mockito
Stream testTurbine + runTest
golden testsscreenshot 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 installDebug o 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 BuildContext a un parámetro global. Pasa dependencias.

  • No uses Any donde Dart usaría dynamic. Modela el tipo.

  • No hagas data class con var por comodidad. El estado inmutable evita bugs.

  • No uses !! para callar al compilador.

  • No conviertas todo en Flow; una función suspend que devuelve un valor es suficiente.

  • No hagas remember de algo que debe sobrevivir a la navegación; usa ViewModel.

  • 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.

Fuente: docs/FLUTTER_TO_KOTLIN.md