# Arquitectura de app (MVVM, repositorio, service locator)

> Cómo estructurar una app cuando CrossFly no impone arquitectura, DI ni binding reactivo.

Source: https://crossfly.dev/es/docs/best-practices/arquitetura-de-app — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# Arquitectura de app

CrossFly **no impone arquitectura** y **no tiene DI ni binding reactivo** (ver
[Estado y flujo de datos](/docs/concepts/state-and-data-flow)). Eso es libertad — y
responsabilidad. A continuación, convenciones **sugeridas** (compatibles, no obligatorias) que escalan. Es el
recorte de tarea del Capítulo 32.

## Estructura de paquetes sugerida
```
app/
 ├─ ui/screens/     → uma tela = uma subclasse de Window
 ├─ viewmodel/      → estado + lógica de apresentação por tela
 ├─ data/repository → fachada sobre dev.crossfly.sqlite / rede
 └─ service/        → serviços de app (singletons) + service locator
```

## Una pantalla = una `Window`
```java
public class ClientesScreen extends Window {
   private final ClientesVM vm = new ClientesVM(Services.get(ClienteRepository.class));
   @Override public void onShow() throws Exception {
      super.onShow();               // arma la UI aquí (bounds válidos)
      // … conecta UI ↔ vm …
      vm.observe(this::render);     // re-renderiza cuando el estado cambia
      vm.carregar();
   }
   @Override public void onRemove() { vm.dispose(); super.onRemove(); }  // libera recursos
}
```

## ViewModel ligero (sin `setState`)
Como no hay binding reactivo, el ViewModel guarda estado y expone un `observe(Runnable)`; la View
**re-renderiza manualmente**:
```java
public class ClientesVM {
   private final ClienteRepository repo;
   private Runnable onChange;
   public List<Cliente> clientes = List.of();
   public ClientesVM(ClienteRepository repo) { this.repo = repo; }
   public void observe(Runnable r) { this.onChange = r; }
   public void carregar() {
      new Thread(() -> {
         List<Cliente> dados = repo.listar();
         Component.onUI(() -> { clientes = dados; if (onChange != null) onChange.run(); });
      }).start();
   }
   public void dispose() { onChange = null; }
}
```

## Repository sobre SQLite
Aísla el acceso a datos en una fachada — la UI nunca habla SQL directo:
```java
public class ClienteRepository {
   public List<Cliente> listar() {
      try (SqliteDb db = SqliteDb.open("app.db");
           SqliteCursor c = db.query("SELECT id, nome FROM cliente")) {
         List<Cliente> out = new ArrayList<>();
         while (c.next()) out.add(new Cliente(c.getLong(0), c.getString(1)));
         return out;
      }
   }
}
```

## En lugar de DI: un service locator simple
La antigua `Registry` fue **eliminada**. Un `Map<Class,Object>` propio resuelve (o inyección por constructor):
```java
public final class Services {
   private static final Map<Class<?>, Object> reg = new HashMap<>();
   public static <T> void put(Class<T> k, T v) { reg.put(k, v); }
   public static <T> T get(Class<T> k) { return k.cast(reg.get(k)); }
}
// en el bootstrap: Services.put(ClienteRepository.class, new ClienteRepository());
```

## Buenas prácticas
- **UI en `onShow()`**, recursos liberados en `onRemove()`.
- Estado en el ViewModel; las mutaciones vuelven a la paint thread vía `onUI`; la View traduce estado→componentes.
- El Repository esconde SQLite/red; la pantalla no conoce SQL.

## Errores comunes
- Buscar DI/observables de Flutter — no existen; usa service locator + `observe(...)` manual.
- Poner SQL/HTTP directo en la `Window` (acopla y traba la UI).
- Guardar estado de app en campos de la `Window` (desaparece al navegar).

## Ver también
- Concepto: [Estado y flujo de datos](/docs/concepts/state-and-data-flow) · [Gestión de estado](/docs/data-and-services/state-management)
