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 inonRemove(). - 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
Windowfields (gone when you navigate).
See also
- Concept: State and data flow · State management
Was this page helpful?Thanks for the feedback!