Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started

This page was machine-translated from Portuguese and may contain errors. The Portuguese version is canonical.

App architecture

CrossFly imposes no architecture and has no DI or reactive binding (see 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

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:

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:

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):

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

Was this page helpful?