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.

7. Managing state

What you’ll learn: CrossFly’s state model — where people coming from Flutter/React need to unlearn. Prerequisites: Handling input.

Complete example (copy, paste, and run)

A lightweight ViewModel (TarefaVM) with a manual observe(Runnable) (there’s no reactive binding): the View re-populates the components when the state changes. A button adds tasks and the list re-renders. Compiles as is.

package com.exemplo;

import java.util.*;
import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;

public class MainWindow extends Window
{
   private final TarefaVM vm = new TarefaVM();
   private final Component listaBox = new Component();
   private int contador = 1;

   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      Button add = new Button("Adicionar tarefa");
      add.onAction(c -> vm.adicionar(new Tarefa("Tarefa " + (contador++))));
      add(LAY_AUTO, add);

      listaBox.setLayout(new VerticalLayout());
      add(LAY_FILL, listaBox);

      vm.observe(this::renderizarLista);   // re-populates when the state changes
      renderizarLista();
   }

   private void renderizarLista()
   {
      listaBox.removeAll();
      for (Tarefa t : vm.tarefas) listaBox.add(LAY_AUTO, new Label(t.titulo));
      listaBox.setValidLayout(false);      // the size changed
      Component.repaint();
   }
}

class Tarefa
{
   final String titulo;
   Tarefa(String titulo) { this.titulo = titulo; }
}

class TarefaVM
{
   private Runnable onChange;
   public List<Tarefa> tarefas = new ArrayList<>();

   public void observe(Runnable r) { this.onChange = r; }
   private void mudou() { if (onChange != null) onChange.run(); }

   public void adicionar(Tarefa t)
   {
      var novo = new ArrayList<>(tarefas); novo.add(t);
      tarefas = novo;
      mudou();                             // notifies the View (on the paint thread)
   }
}

The snippets below show only the core (what goes inside onShow()). To run them, put them in the MainWindow skeleton above.

There’s no reactive binding

Changing the model does not update the UI on its own. State lives in fields (or in a lightweight ViewModel), and the View translates state → components manually, calling repaint() (and setValidLayout(false) if the size changed).

A lightweight ViewModel with observe

public class TarefaVM {
   private Runnable onChange;
   public List<Tarefa> tarefas = List.of();
   public void observe(Runnable r) { this.onChange = r; }
   private void mudou() { if (onChange != null) onChange.run(); }

   public void adicionar(Tarefa t) {
      var novo = new ArrayList<>(tarefas); novo.add(t);
      tarefas = novo;
      mudou();                    // notifies the View (on the paint thread)
   }
}

The View reacts

@Override public void onShow() throws Exception {
   super.onShow();
   // … builds UI …
   vm.observe(this::renderizarLista);   // re-populates the components when the state changes
   vm.carregar();                       // triggers the load (in background — next chapter)
}

private void renderizarLista() {
   listaBox.removeAll();
   for (Tarefa t : vm.tarefas) { CardTarefa card = new CardTarefa(); listaBox.add(LAY_AUTO, card); card.bind(t); }
   listaBox.setValidLayout(false);
   Component.repaint();
}

Rule: mutations and notifications always on the paint thread. If the data came from the background, enter Component.onUI(...) before touching the state the View reads (see the next chapter).

What you build in this step

The TarefaVM with the screen re-rendering when the state changes.

Recap

No reactive: state in fields/ViewModel; the View translates state→components with repaint(); a manual observe(Runnable) replaces the binding.

Next steps

→ Loading data

See also

Was this page helpful?