Aquí tienes una tarea de la que habrás escrito veinte versiones: una pantalla con el listado de productos. Cargar desde el servidor, gestionar el error, pintar la lista.
Se la dimos a un agente — Claude Opus 5 — y esto es lo que devolvió.
class ProductsState {
final List<Product>? items;
final bool isLoading;
final Exception? error;
}
Parece algo que habrías escrito tú. Y esa es justo la cuestión: estos modelos se entrenaron con lo que todos llevamos años escribiendo a mano.
Ahora echa la cuenta. Tres campos: un bool, una lista nullable y un error nullable. Cada uno con dos estados que importan. Dos por dos por dos: ocho combinaciones. ¿Cuántas significan algo?
isLoading | items | error | Qué significa |
|---|---|---|---|
true | null | null | Cargando |
false | null | con valor | Ha fallado |
false | con valor | null | Cargado |
true | con valor | null | ? |
true | null | con valor | ? |
true | con valor | con valor | ? |
false | con valor | con valor | ? |
false | null | null | ? |
Tres. Las otras cinco no son errores de compilación. Son código válido. Compila, pasa la revisión, llega a producción y se queda ahí. isLoading está a true y al lado hay un error: ¿qué pintas? O está todo vacío: no cargamos, no hay lista, no hay error. ¿Todavía no hemos empezado, o fuimos y volvimos sin nada? El código no lo sabe. Lo adivina, o lo adivina la siguiente persona que abra el fichero.
Lo esencial
- Un estado imposible es aquel que tu tipo permite y tu dominio prohíbe. Es lo mismo que un invariante roto, visto desde el otro lado.
- El coste no son los bugs en tiempo de ejecución. Es una convención que nadie escribió, ramas que nadie llegó a considerar y un impuesto que pagas en cada lectura, no una vez por incidente.
- El invariante siempre existe. La única pregunta es si lo sostiene una persona — un comentario, una página del wiki, un
assertque no se ejecuta en release — o si no hay nada que sostener porque romperlo no compila. enum Statusmás campos nullable lo empeora, no lo mejora: doce combinaciones en vez de ocho, y siguen siendo tres las que significan algo.- Una sealed class con campos no nullable deja exactamente los estados con sentido y hace que el resto no se pueda construir. Ni booleanos, ni nullables, ni
!. - La exhaustividad es la recompensa. Añades un cuarto estado y el compilador enumera todos los sitios que no lo tratan, que es retroalimentación sobre la que un agente puede actuar, no un muro contra el que choca.
- Una sola rama
_ =>desactiva todo eso, en silencio. Es la única regla de revisión que merece la pena llevarse de este artículo. - Los tipos algebraicos no lo arreglan todo. Eliminan las combinaciones imposibles de estados, no los valores imposibles, y no hacen nada en la frontera del sistema.
Estados imposibles y su nombre más antiguo
Primero el vocabulario, porque hay dos palabras para una misma cosa.
Un estado imposible es aquel que tu tipo permite y tu dominio prohíbe. Una pantalla no puede estar cargando y mostrando un error a la vez. El tipo lo permite. Eso es un agujero.
El nombre más antiguo y más preciso es invariante: una regla que tiene que cumplirse siempre. Si estamos cargando, todavía no hay datos. Si hay un error, tiene un mensaje. Un estado imposible es simplemente un invariante roto. El mismo hecho desde dos lados: el invariante es cómo debería ser, y el estado imposible es lo que te queda cuando no lo fue.
Quédate con esa palabra. Todo gira a su alrededor.
Por qué esto es un problema, y por qué la explicación habitual falla
Lo que se suele decir es que los estados imposibles son bugs que no revientan: pintan la pantalla equivocada sin hacer ruido. Suena convincente, y si el problema fuera ese, la respuesta sería barata: tests, QA, una alerta sobre la pantalla vacía. Lo pillas, lo arreglas, sigues.
El problema no es ese. No va de tiempo de ejecución en absoluto. Son tres costes, y los tres están en tu código ahora mismo, aunque la aplicación se comporte perfectamente.
Uno: la convención que nadie escribió. Mira otra vez la clase del agente. ¿Dónde dice que cuando isLoading es true no debes leer items? En ningún sitio. ¿Dónde dice que error e items nunca están rellenos a la vez? En ningún sitio. Esas reglas existen y la gente confía en ellas, pero no están en el tipo ni en un comentario. Viven en la cabeza de quien escribió la clase, y todos los demás las reconstruyen a partir del código, leyendo los if y deduciendo la intención. Cinco minutos por fichero, para cada persona que lo abra.
Dos: el número de ramas. Ocho combinaciones son ocho casos, y con cada uno alguien tiene que hacer algo: tratarlo o descartarlo deliberadamente. Se tratan tres. Las otras cinco ni se tratan ni se descartan: nunca se consideraron. Esa distinción importa, porque un caso descartado es una decisión y uno no considerado es un agujero.
Y la cuenta crece multiplicando.
| Campos que viajan juntos | Combinaciones | Con sentido |
|---|---|---|
| 3 | 8 | 3 |
| 4 | 16 | 3–4 |
| 5 | 32 | 3–4 |
| 6 | 64 | 3–4 |
Tres: pagas en cada lectura, no una vez por bug. Este es el importante. Un bug se arregla una vez. Una convención que no está en el tipo la vuelve a deducir todo el que toque esa pantalla: en la revisión, al añadir una funcionalidad, mientras investiga un incidente. Eso no es un coste puntual, es un impuesto.
De ahí sale también el parche habitual. La gente tapa estos agujeros con condiciones: aquí comprobamos que no estamos cargando, allí que el error no es nulo. Cada uno de esos if es un parche sobre un agujero que abriste tú al declarar tres campos independientes. Cinco agujeros, normalmente dos parches: los que ya se dispararon.
Y de paso, fíjate: nada de esto revienta. Ni informe de fallo, ni traza de pila, ni nada en tu sistema de errores. Así que lo de que la monitorización lo pillará no sirve aquí. Pero eso es un efecto secundario, no el fondo del asunto.
Lo que ha cambiado en los últimos dos años es el volumen. Antes esta clase aparecía una vez por semana, escrita por una persona que dedicó al menos treinta segundos a pensar en los campos. Ahora aparece en doce segundos y nadie la ha leído: ni el agente ni tú. Y quien revisa es una persona que mira tres campos, no alguien que sostiene ocho combinaciones en la cabeza.
Quién sostiene el invariante
Así que la pregunta nunca es si tienes un invariante. Siempre lo tienes. La pregunta es quién responde por él.
Normalmente una persona. Un comentario encima de la clase. Una página del wiki que nadie abre desde hace dos años. Una convención de revisión que recuerdan dos de cada cinco. En el mejor de los casos, un assert en el constructor.
Un assert no es un invariante en Dart
Las instrucciones assert se eliminan de las compilaciones de release. No es que se salten: la condición no llega a evaluarse, y sus argumentos tampoco, por eso una comprobación cara dentro de un assert no cuesta nada en producción. La consecuencia práctica es justo la que se olvida: un invariante que defendiste con un assert no existe delante de tus usuarios. Se cumplía durante el desarrollo, donde ya estabas mirando, y falta exactamente en el único sitio donde no miras. Un assert es una ayuda de depuración, no una restricción.
Todo eso es un invariante que alguien sostiene: a mano, con atención, de memoria. La alternativa es organizar las cosas de forma que no haya nada que sostener, porque romperlo no es expresable en el tipo. No es que comprobemos que esto no puede pasar, sino que esto no compila.
Y por eso dejó de ser una distinción académica. Un invariante sostenido por una persona funciona exactamente mientras el código se escriba a velocidad humana. Un agente no ha leído tu comentario, no ha abierto tu wiki y no ha estado en tu revisión de código. Las convenciones no escalan a la velocidad de generación. Los compiladores sí.
Tipos de datos algebraicos, de una pasada
Una advertencia antes de la teoría, para desarmar la mitad de las objeciones. El ejemplo es deliberadamente el más obvio posible. Cargando, error, éxito es la ilustración más gastada de los tipos algebraicos que hay en internet: está en todos los tutoriales. La idea tiene un eslogan famoso, make illegal states unrepresentable, que se suele atribuir a las charlas Effective ML de Yaron Minsky, y lleva más de una década por ahí. Aquí no se descubre nada. Lo interesante es de dónde salió esa clase al principio del artículo, y qué haces cuando código así te llega por lotes.
Ahora la definición. Sin teoría de categorías.
Un producto es «y». Una clase corriente con campos: una cadena y un número y un booleano. El número de valores posibles es el producto de las cuentas. De ahí salieron los ocho: no es retórica, es aritmética. Cada campo que añades multiplica.
Una suma es «o». El valor es esto o aquello, y no hay una tercera opción. La suma más sencilla que todo el mundo ya conoce es un enum. Una sealed class es esa misma suma, solo que cada caso puede llevar sus propios datos. Aquí las cuentas se suman: cargando es un valor, ha fallado son tantos como errores haya, cargado son tantos como listas haya. Nada se multiplica gratis.
La idea entera en una frase: no puedes construir el tipo de forma que represente un estado que nunca fue válido.
"¿Y por qué no un enum y ya está?"
La objeción obvia es que no hacen falta sealed classes: añade un enum Status con tres valores y deja de complicarlo. El instinto es razonable, y no funciona.
enum Status { loading, error, success }
class ProductsState {
final Status status;
final List<Product>? items;
final Exception? error;
}
Cuenta otra vez: tres valores de estado, por dos estados de lista, por dos estados de error. Doce combinaciones. Eran ocho. Siguen siendo tres las que significan algo. Aritméticamente ha empeorado: nueve combinaciones basura en vez de cinco.
Y el problema original sigue intacto. status es success e items es null: compila perfectamente. En la interfaz sigues escribiendo items!, jurándole al compilador que seguro que los datos están ahí. Una promesa no es una garantía. Es una comprobación desactivada con sintaxis agradable.
La diferencia está en lo que lleva la suma. Un enum es una suma sin datos: te dice en qué estado estás, pero los datos quedan al lado, atados al estado solo por tu disciplina. Una sealed class es una suma con datos: la lista existe exactamente donde significa algo, y en ningún otro sitio.
Así que la regla es: si tus estados no llevan datos, usa un enum, es más corto. En cuanto uno solo de ellos lleva carga útil, el enum deja de ayudar, y cada ! de tu interfaz es el recibo.
Es una idea vieja. ML, de donde viene, tiene cincuenta años; Haskell, OCaml y F# la tienen desde hace décadas. En los lenguajes mayoritarios ya está prácticamente en todas partes: Dart la incorporó en la versión 3, en 2023. Lo que cambió no es la idea sino el argumento a su favor. Los tipos algebraicos se vendían por elegancia, y los argumentos estéticos pierden contra los sprints. El argumento ahora es que el código se escribe más rápido de lo que se lee, el único revisor que sigue el ritmo de la generación es el compilador, y todo lo que no expresaste en el tipo lo expresaste como una esperanza.
La reescritura
Tres pasos, unos dos minutos de trabajo.
Paso uno: escribe los estados con palabras, antes de escribir una sola línea de código. La pantalla está cargando. La pantalla ha fallado. La pantalla tiene una lista. Ya está: tres. Si en este paso te salen siete, casi con seguridad estás escribiendo combinaciones de flags en vez de estados. Un estado es algo que puedes nombrar con una palabra y señalar en una maqueta.
Paso dos: cada estado se convierte en su propia clase.
sealed class ProductsState {
const ProductsState();
}
final class Loading extends ProductsState {
const Loading();
}
final class Failed extends ProductsState {
const Failed(this.error);
final Exception error;
}
final class Loaded extends ProductsState {
const Loaded(this.items);
final List<Product> items;
}
Mira lo que ha pasado. Failed lleva un error, y no es nullable: el estado «hemos fallado pero no hay error» deja de existir, y físicamente no puedes construirlo. Loaded lleva una lista, tampoco nullable. Loading no lleva nada, porque durante la carga no hay datos, y mantener un campo para ellos es precisamente la invitación al estado imposible.
Fíjate en lo que no está. Ni un solo booleano. Ni un solo campo nullable. Ese es todo el refactor; el resto es sintaxis.
Un detalle propio de Dart: sealed restringe la herencia a la misma librería, que en la práctica es el mismo fichero salvo que la partas a propósito con part. Eso no es pedantería, es el mecanismo: para comprobar la exhaustividad el compilador necesita la lista completa de casos, y solo puede garantizarla dentro de la librería. De ahí la convención de que, haya los casos que haya, vivan juntos en un fichero. La primera vez resulta raro; después, cómodo: todo el espacio de estados de la pantalla se ve de un vistazo.
Paso tres: la interfaz.
Widget build(BuildContext context) {
return switch (state) {
Loading() => const AppSpinner(),
Failed(:final error) => ErrorView(error),
Loaded(:final items) => ItemList(items),
};
}
Ni un if. Ni una comprobación de null. Los datos se desestructuran en el patrón — :final items — y llegan ya tipados. Dentro de la rama Loaded la lista existe, en vez de «podría existir». Compáralo con lo que tendrías con flags: una cadena de comprobaciones cuyo orden importa y cuya razón ya no recuerda nadie.
Cómo hacerlo en un proyecto con cuarenta pantallas
No reescribas todo. La regla: pantalla nueva, sealed desde el principio; pantalla vieja, cuando ya estés dentro arreglando algo. Una pantalla que nadie ha tocado en dos años, no la toques. Es muy posible que tenga estados imposibles, pero o ya se dispararon o no se dispararán nunca.
Para las que sí toques, la prioridad se reduce a una cosa: desde cuántos sitios se lee ese estado. Un switch en un widget: reescribirlo casi no compensa, porque de todos modos lo ves todo. Un estado que se lee desde seis sitios, entre interfaz, analítica, logs y un manejador de push, es donde la exhaustividad se gana el sueldo, porque esos son justo los cinco de seis que se olvidan. Si tu capa de gestión de estado ya canaliza la pantalla por un único flujo, ese es el sitio más barato para empezar.
El mismo tipo en otros cuatro lenguajes
No porque haga falta un segundo lenguaje, sino para que quede claro que esto no es un dialecto de Dart ni una moda de la comunidad Flutter. El mismo tipo, tres estados, los datos atados al estado.
Kotlin.
sealed interface ProductsState
data object Loading : ProductsState
data class Failed(
val error: AppError,
) : ProductsState
data class Loaded(
val items: List<Product>,
) : ProductsState
when usado como expresión está obligado a cubrir todas las ramas: exactamente nuestro switch.
Rust. Más interesante, porque es el enum incorporado:
enum ProductsState {
Loading,
Failed(AppError),
Loaded(Vec<Product>),
}
En Rust un enum es una suma con datos desde el principio, sin ceremonia añadida. Si te dejas un caso en un match, no compila.
Swift, lo mismo con valores asociados:
enum ProductsState {
case loading
case failed(AppError)
case loaded([Product])
}
Y el exótico, Idris: la sintaxis de ML de la que desciende todo lo anterior.
data ProductsState
= Loading
| Failed AppError
| Loaded (List Product)
Mira la barra vertical. Se lee como «o». El tipo suma está escrito literalmente con un símbolo de «o» — esto es una suma mucho antes de que nadie lo llamara sealed class.
Idris hace además algo que ninguno de los otros cuatro puede:
import Data.Vect
data ProductsState : Type where
Loading : ProductsState
Failed : AppError -> ProductsState
Loaded : Vect (S n) Product -> ProductsState
Vect (S n) en el tipo significa una lista con al menos un elemento garantizado. No es que lo hayamos comprobado ni que lo hayamos acordado: simplemente no puedes construir Loaded con una lista vacía. Recuerda este fragmento: volvemos a él al final.
Una idea, cinco lenguajes, exhaustividad en todos. Si no trabajas con Dart, todo lo que sigue funciona igual para ti.
La exhaustividad, que es para lo que era todo esto
Un mes después llega un requisito: cuando la lista llegue vacía, muestra una pantalla de estado vacío con un botón. Eso no es un error ni es éxito con datos. Es un cuarto estado.
final class Empty extends ProductsState {
const Empty();
}
Una línea. No ha cambiado nada más. Y el proyecto no compila:
The type 'ProductsState' is not exhaustively matched by the switch
cases since it doesn't match 'Empty()'.
El compilador enumera todos los sitios donde el estado nuevo no se trata. No uno de ellos: todos.
Ahora, por qué esto pertenece a un artículo sobre código escrito por IA. «Añade un estado vacío a la pantalla de listado» es exactamente el tamaño de tarea que la gente le pasa a un agente sin mirar. El agente añadirá la clase, actualizará el switch que tenía en contexto y no actualizará los otros dos: el de la analítica y el que hay detrás del botón de recargar. No estaban en el prompt, así que para él no existen.
Con flags, eso llega a producción. No hay diff que lo pille, porque el diff solo contiene lo que el agente cambió. Los tests están en verde, porque un estado que no existía hace un mes no tiene tests por definición. Te enteras por un usuario, seis semanas después, en forma de «a veces me sale una pantalla en blanco».
Con sealed classes no llega a ninguna parte. No compila.
Y aquí está lo que merece la pena llevarse. La exhaustividad no es protección frente a la IA. Es retroalimentación para ella. Lo que hace malo a un agente en esto es que no tiene acceso a la lista de «y no te olvides tampoco de esto de aquí» que tú llevas en la cabeza. El compilador emite esa lista en formato legible por máquina, con ficheros y números de línea. El agente choca contra el rojo y arregla los tres, que es algo que hace genuinamente bien porque le han dicho exactamente dónde. No le estás impidiendo trabajar: le estás dando lo que le faltaba. El mismo mecanismo ayuda a una persona una vez por semana y a un agente cada veinte minutos.
La única línea que lo desactiva todo
return switch (state) {
Loading() => const AppSpinner(),
_ => ItemList(state.items),
};
Un guion bajo y la exhaustividad ha muerto. En silencio, sin ningún aviso. Una rama que lo captura todo trata todos los casos por definición, así que al compilador no le queda nada que comprobar. Añade un quinto estado, un sexto, un décimo: compila y cae aquí en silencio.
Si te llevas exactamente una regla de revisión de este artículo, llévate esta:
Un switch sobre un tipo sealed no debería tener rama por defecto.
Es la única línea de todo esto que hay que comprobar con los ojos, y una regla de lint la detecta para que no tengas ni que hacerlo.
Dónde no ayudan los tipos algebraicos
Cuatro sitios donde no funcionan o funcionan en tu contra. Sin esta sección lo demás es un sermón.
Una suma no cura un producto. Has partido la pantalla en tres estados, y eso está bien. Pero Loaded sigue siendo una clase corriente con campos, y si son doce y la mitad son nullable, has movido el pantano un piso más abajo. Los tipos algebraicos van de qué estados existen, no de qué hay dentro de un estado.
Estados que se solapan: el error más común al cambiar de enfoque. Llega un requisito de pull-to-refresh: la lista ya está en pantalla y estás pidiendo una nueva. ¿Eso es Loading o Loaded? La respuesta ingenua es un caso Refreshing con items dentro. Una semana después tienes Refreshing, RefreshingAfterError y FailedButHasCache: la misma explosión combinatoria, ahora en clases, que es peor que con flags porque es más verbosa.
La respuesta correcta molesta a la gente:
final class Loaded extends ProductsState {
const Loaded(this.items, {this.isRefreshing = false});
final List<Product> items;
final bool isRefreshing;
}
Sí, el booleano ha vuelto, y está bien. Ahora vive dentro de un estado donde significa algo: «tenemos una lista y la estamos refrescando» es una situación real que necesitas poder expresar. Y «refrescando sin lista» ya no existe, porque no hay ningún sitio donde ponerlo.
Sumas para lo mutuamente excluyente, productos para lo que coexiste
Esta es la regla que decide si el refactor ayuda o perjudica. Dos cosas que nunca pueden ser ciertas a la vez piden una suma: casos separados de un tipo sealed. Dos cosas que habitualmente son ciertas a la vez piden un producto: campos uno al lado del otro dentro de un mismo caso. Confundirlos es la principal forma de acabar peor que al empezar, porque estados mutuamente excluyentes modelados como booleanos paralelos te dan el problema de las ocho combinaciones, y hechos que coexisten modelados como casos separados te dan una clase por combinación.
Los tipos no sostienen todos los invariantes, y esto importa más que el resto. Una sealed class expresa maravillosamente un invariante estructural: qué estados existen y qué datos viajan con qué estado. No expresa nada sobre invariantes de valor: esta lista está ordenada, la fecha de inicio precede a la de fin, las líneas suman el total, esta cadena es un correo válido. Todo eso lo sigue sosteniendo una persona, y ningún sealed ayuda.
Ese fragmento de Idris es justamente el contraejemplo. Vect (S n) — no vacía, en el tipo — es un invariante de valor empujado dentro del sistema de tipos. Eso se llama tipos dependientes, y podrías expresar «ordenada» o «inicio antes que fin» de la misma forma. El precio es un lenguaje en el que no escribes y, siendo realistas, no vas a escribir. En la práctica se recurre a otra técnica: un constructor privado con validación, para que el tipo no pueda crearse en una forma inválida.
En el mismo saco están las fronteras del sistema. El JSON que llega por la red no está tipado. Los tipos algebraicos empiezan después del parseo, y una sealed class no te salvará cuando el backend mande "succes" con una sola «s»: de eso te salva una deserialización explícita con claves escritas a mano. La frase que merece la pena guardar: los tipos algebraicos eliminan las combinaciones imposibles de estados, no los valores imposibles.
Y la sobreingeniería. Una sealed class con dos casos y sin datos es un enum, y el enum se lee más rápido. Una sealed class con un caso es una clase. Un booleano honesto sigue siendo un booleano honesto: isSelected en una casilla son exactamente dos estados, ambos válidos, y ahí un tipo algebraico no mejora nada. La prueba es simple: cuenta las combinaciones y tacha las que no tienen sentido. Si no hay nada que tachar, déjalo en paz.
Por último, el coste honesto: es verboso. Cuatro estados en Dart son unas treinta líneas frente a cuatro campos. La mitad del argumento que dice que se tarda más en escribir no sobrevive a 2026: eso es exactamente el trabajo que un agente hace en segundos y sin errores. La otra mitad, que se tarda más en leer, no ha muerto, y ese es el precio que pagas.
Encuentra los tuyos en media hora
Revisar una clase de estado
Diez comprobaciones en tres grupos. Todo lo que quede sin marcar es una decisión que alguien todavía no ha tomado.
El tipo en sí
- Las situaciones mutuamente excluyentes son casos separados de un tipo sealed, no booleanos paralelos
- Cada campo dentro de un caso es no nullable, o el null significa algo concreto y está documentado
- Los datos viven en el estado al que pertenecen, no al lado de un enum de estado
- Los hechos que coexisten son campos dentro de un mismo caso, no un caso por combinación
Cada switch sobre él
- Ninguna rama por defecto ni patrón de guion bajo sobre un tipo sealed
- Ningún desempaquetado forzado de un campo que el estado ya debería garantizar
- Están cubiertos todos los lectores del estado, incluidos la analítica, los logs y los manejadores en segundo plano
Lo que los tipos no harán por ti
- Los invariantes de valor — ordenado, no vacío, dentro de rango, bien formado — se imponen en un constructor, no se dan por supuestos
- El parseo en la frontera del sistema valida de forma explícita en lugar de fiarse de la forma de los datos
- Los assert se tratan como ayuda de depuración, porque no se ejecutan en las compilaciones de release
La versión corta
Un estado imposible es aquel que tu tipo permite y tu dominio prohíbe, que es lo mismo que un invariante roto. Y sale caro no porque rompa algo en tiempo de ejecución, sino porque cada convención que no está en el tipo la vuelve a deducir alguien en cada lectura.
El invariante siempre existe. La única pregunta es si lo sostiene una persona — con un comentario, una página de wiki o un assert que no se ejecuta en release — o si no hay nada que sostener porque romperlo no compila.
Tres campos que viajan juntos, ocho combinaciones, tres con sentido. Una sealed class con campos no nullable deja exactamente esas tres y hace que las otras cinco no se puedan construir. La exhaustividad es la recompensa: convierte «alguien se olvidó de actualizar un caso» en «el proyecto no compila», y funciona igual sea quien sea el que se olvidó. Y una rama por defecto lo desactiva todo, en silencio.
Y el cambio de fondo. Antes los tipos eran la forma de explicar tu razonamiento a un colega que abriría el fichero seis meses después. Ahora son la única forma de explicárselo a algo que escribe código más rápido de lo que tú lees. El oficio no ha desaparecido: ha subido un nivel. Antes escribías la implementación. Ahora escribes las fronteras dentro de las que la implementación tiene que quedarse, y rellenar lo de dentro puede hacerlo cualquiera. Incluida una máquina.
Preguntas frecuentes
Si quieres la lista más amplia de lo que hacen los agentes sin supervisión con el código Flutter, esto es el número tres de nuestro desglose de siete puntos, y el proceso con el que lo evitamos lo contamos en cómo construimos con IA. Este artículo acompaña a nuestro episodio en vídeo sobre el mismo tema; el anterior fue tu SSL pinning probablemente no funciona.
Haz el ejercicio y cuéntanos cuántas filas tachaste y en qué pantalla. Nuestra apuesta es que la mayoría acaba en los mismos tres estados que nosotros.
Ilya Nixan es fundador y lead developer en Nerdy Production, una agencia Flutter-first que construye y mantiene apps para fintech, salud y retail. También hacemos auditoría de código escrito por IA para equipos cuya base de código creció más rápido de lo que nadie podía revisar.
