# Arquitetura de app (MVVM, repositório, service locator)

> Como estruturar um app quando o CrossFly não impõe arquitetura, DI nem binding reativo.

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

# Arquitetura de app

O CrossFly **não impõe arquitetura** e **não tem DI nem binding reativo** (ver
[Estado e fluxo de dados](/docs/concepts/state-and-data-flow)). Isso é liberdade — e
responsabilidade. Abaixo, convenções **sugeridas** (compatíveis, não obrigatórias) que escalam. É o
recorte de tarefa do Capítulo 32.

## Estrutura de pacotes 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
```

## Uma tela = uma `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();               // monte a UI aqui (bounds válidos)
      // … liga UI ↔ vm …
      vm.observe(this::render);     // re-renderiza quando o estado muda
      vm.carregar();
   }
   @Override public void onRemove() { vm.dispose(); super.onRemove(); }  // libere recursos
}
```

## ViewModel leve (sem `setState`)
Como não há binding reativo, o ViewModel guarda estado e expõe um `observe(Runnable)`; a 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
Isole o acesso a dados numa fachada — a UI nunca fala SQL direto:
```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;
      }
   }
}
```

## No lugar de DI: um service locator simples
A antiga `Registry` foi **removida**. Um `Map<Class,Object>` próprio resolve (ou injeção por construtor):
```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)); }
}
// no bootstrap: Services.put(ClienteRepository.class, new ClienteRepository());
```

## Boas práticas
- **UI em `onShow()`**, recursos liberados em `onRemove()`.
- Estado na ViewModel; mutações voltam à paint thread via `onUI`; a View traduz estado→componentes.
- Repository esconde SQLite/rede; a tela não conhece SQL.

## Erros comuns
- Procurar DI/observáveis do Flutter — não existem; use service locator + `observe(...)` manual.
- Colocar SQL/HTTP direto na `Window` (acopla e trava a UI).
- Guardar estado de app em campos da `Window` (some ao navegar).

## Veja também
- Conceito: [Estado e fluxo de dados](/docs/concepts/state-and-data-flow) · [Gerência de estado](/docs/data-and-services/state-management)
