# App architecture (MVVM, repository, service locator)

> How to structure an app when CrossFly imposes no architecture, DI, or reactive binding.

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

# App architecture

CrossFly **imposes no architecture** and **has no DI or reactive binding** (see
[State and data flow](/docs/concepts/state-and-data-flow)). That's freedom — and
responsibility. Below are **suggested** conventions (compatible, not mandatory) that scale. This is the
task-oriented cut of Chapter 32.

## Suggested package structure
```
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
```

## One screen = one `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();               // build the UI here (valid bounds)
      // … links UI ↔ vm …
      vm.observe(this::render);     // re-renders when the state changes
      vm.carregar();
   }
   @Override public void onRemove() { vm.dispose(); super.onRemove(); }  // release resources
}
```

## Lightweight ViewModel (no `setState`)
Since there's no reactive binding, the ViewModel holds state and exposes an `observe(Runnable)`; the
View **re-renders manually**:
```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 over SQLite
Isolate data access in a facade — the UI never speaks SQL directly:
```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;
      }
   }
}
```

## In place of DI: a simple service locator
The old `Registry` was **removed**. Your own `Map<Class,Object>` resolves it (or constructor injection):
```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)); }
}
// at bootstrap: Services.put(ClienteRepository.class, new ClienteRepository());
```

## Best practices
- **UI in `onShow()`**, resources released in `onRemove()`.
- State in the ViewModel; mutations return to the paint thread via `onUI`; the View translates state→components.
- The Repository hides SQLite/network; the screen knows no SQL.

## Common mistakes
- Looking for Flutter's DI/observables — they don't exist; use a service locator + manual `observe(...)`.
- Putting SQL/HTTP directly in the `Window` (couples and freezes the UI).
- Keeping app state in `Window` fields (gone when you navigate).

## See also
- Concept: [State and data flow](/docs/concepts/state-and-data-flow) · [State management](/docs/data-and-services/state-management)
