Pular para o conteúdo
CrossFly é 100% open sourceSiga no GitHub
CrossFlyCrossFly
Começar
Todos os exemplos
AplicativosDadosListasFormulários2025 linhas · 11 arquivos

Depot — offline-first CRUD

One shell, three listings over a local database, a detail and a long form. A thing you navigate away from is a Window; a thing you switch between is not.

O que isto mostra

Depot — the offline-first CRUD app, and the entry point of this example.

gradlew lwjgl3:run --args="classname dev.cf.app.crud.CrudApp"

The shape

One shell Window hosting four destinations, three of which are listings over a local SQLite database, plus a detail overlay and a long form. It is the shape most business apps end up being, and the four decisions below are the ones that are expensive to change later.

1 · The shell is one Window; the destinations are Components

TabBar hosts them and keeps them alive across switches, so a tab the user comes back to is where they left it. Overlays — PartDetailScreen, PartFormScreen — are separate Windows shown on top (new X().show(), back is hide()). The rule that follows from this is short: a thing you navigate away from is a Window; a thing you switch between is not.

2 · The store is the source of truth, and it is local

Every screen reads Repository and nothing else. There is no in-memory cache next to it, because two caches disagree the moment one screen writes. After a write, screens call refreshAll() and re-read — which is cheap, correct, and does not need a change-event system to be invented for it.

3 · The first run has a screen of its own

FirstLoadScreen holds navigation while the store fills, and it is the only screen that can fail visibly. Every other screen in the app is instant by construction.

4 · Unsent work is visible

A local edit shows a "not sent" badge on the card, on the detail and as a number on OutboxTab. The user never has to guess whether something arrived.

What is deliberately missing

Login, permissions, a real transport, conflict resolution, and paging. Each is a genuine subject and each would double this file's size without teaching anything about the shape. The seam for the transport is flushOutbox(); the seam for paging is Items, which counts and renders rather than taking a list, precisely so a listing can show more than it has loaded.

Rodar na sua máquina

Todo exemplo é uma Window comum, então o launcher só precisa do nome dela.

gradlew lwjgl3:run --args="classname dev.cf.app.crud.CrudApp"
Editar o exemplo

Java é compilado antes de rodar, então o navegador não consegue reconstruir uma edição sozinho — o código que você vê aqui virou JavaScript pelo TeaVM antes de ser publicado. Copie o arquivo para um projeto e ele roda sem mudanças nas quatro plataformas; o comando acima é toda a preparação.

Rodando aquiCrossFly 0.1.32
Iniciando o aplicativo…

Compilado para JavaScript pelo TeaVM — é o mesmo código, não um vídeo dele. · Todos os exemplos

O que ele usa

Tipos do framework que este exemplo toca. Os que têm link têm página de referência.

AlertBadgeButtonCardComponentDataGridDateTimeUtilsDebugDeviceDropdownGapGridLayoutHintLabelListingMainMaterialIconPalColorProgressRelativeScreenSkinSqliteCursorSqliteDbStylesTabBarTextAreaTextFieldVerticalLayoutWidgetsWindow

O código

Completo, sem cortes. É o arquivo, não um trecho dele.

Bruto
dev/cf/app/crud/CrudApp.java
package dev.cf.app.crud;

import dev.cf.Described;
import dev.cf.app.AppBar;
import dev.cf.app.AppTheme;
import dev.crossfly.ui.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.skin.Skin;
import dev.crossfly.ui.style.*;

/**
 * <b>Depot</b> — the offline-first CRUD app, and the entry point of this example.
 *
 * <pre>
 * gradlew lwjgl3:run --args="classname dev.cf.app.crud.CrudApp"
 * </pre>
 *
 * <h2>The shape</h2>
 *
 * <p>One shell Window hosting four destinations, three of which are listings over a local SQLite
 * database, plus a detail overlay and a long form. It is the shape most business apps end up
 * being, and the four decisions below are the ones that are expensive to change later.</p>
 *
 * <h3>1 · The shell is one Window; the destinations are Components</h3>
 *
 * <p>{@link TabBar} hosts them and keeps them alive across switches, so a tab the user comes back to
 * is where they left it. Overlays — {@link PartDetailScreen}, {@link PartFormScreen} — are separate
 * Windows shown on top ({@code new X().show()}, back is {@code hide()}). The rule that follows from
 * this is short: <b>a thing you navigate away from is a Window; a thing you switch between is
 * not.</b></p>
 *
 * <h3>2 · The store is the source of truth, and it is local</h3>
 *
 * <p>Every screen reads {@link Repository} and nothing else. There is no in-memory cache next to it,
 * because two caches disagree the moment one screen writes. After a write, screens call
 * {@link #refreshAll()} and re-read — which is cheap, correct, and does not need a change-event
 * system to be invented for it.</p>
 *
 * <h3>3 · The first run has a screen of its own</h3>
 *
 * <p>{@link FirstLoadScreen} holds navigation while the store fills, and it is the only screen that
 * can fail visibly. Every other screen in the app is instant by construction.</p>
 *
 * <h3>4 · Unsent work is visible</h3>
 *
 * <p>A local edit shows a "not sent" badge on the card, on the detail and as a number on
 * {@link OutboxTab}. The user never has to guess whether something arrived.</p>
 *
 * <h2>What is deliberately missing</h2>
 *
 * <p>Login, permissions, a real transport, conflict resolution, and paging. Each is a genuine
 * subject and each would double this file's size without teaching anything about the shape. The
 * seam for the transport is {@link Repository#flushOutbox()}; the seam for paging is
 * {@link Listing.Items}, which counts and renders rather than taking a list, precisely so a listing
 * can show more than it has loaded.</p>
 */
public class CrudApp extends Window implements Described
{
   /** Destination indices, in bottom-nav order. */
   public static final int TAB_PARTS = 0, TAB_SUPPLIERS = 1, TAB_ORDERS = 2, TAB_OUTBOX = 3;

   private static final int IC_PARTS = 0xE1DB, IC_SUPPLIERS = 0xE8D1, IC_ORDERS = 0xE8CC,
      IC_OUTBOX = 0xE2C3;

   /** Bottom-nav glyphs read better a little larger than body text. */
   private static final float NAV_ICON = 1.5f;

   private TabBar tabs;
   private PartsTab parts;
   private SuppliersTab suppliers;
   private OrdersTab orders;
   private OutboxTab outbox;

   @Override
   public String title()
   {
      return "Depot — offline-first CRUD";
   }

   @Override
   public String note()
   {
      return "One shell, three listings over a local database, a detail and a long form. A"
         + " thing you navigate away from is a Window; a thing you switch between is not.";
   }

   /** Hands the palette back to whatever was in force before this app opened. */
   @Override
   public void onRemove()
   {
      super.onRemove();
      AppTheme.leave();
   }

   @Override
   public void onAdd(boolean firstTime) throws Exception
   {
      if (!firstTime)
         return;
      // The brand FIRST: fullscreenColor below reads a token, and reading it before the palette
      // is declared bakes in the skin's neutral grey for the life of the window.
      AppTheme.enter();
      // The PAGE, not the surface. Cards, buttons and fields are all drawn on the surface, so a
      // page painted the same colour makes every one of their edges disappear. Read through
      // onRestyle so a light/dark flip re-derives it instead of keeping the old mode's value.
      fullscreenColor = AppTheme.pageColor();
      onRestyle(() -> fullscreenColor = AppTheme.pageColor());
      setLayout(new VerticalLayout(Position.START, Position.START));
      // Nothing is built until the store answers. The gate runs the check off the paint thread and
      // shows itself only when there is work; on a warm store this callback fires within a frame or
      // two and the user never sees a loading screen.
      FirstLoadScreen.gate(this::buildShell);
   }

   private void buildShell()
   {
      try
      {
         parts = new PartsTab(this);
         suppliers = new SuppliersTab(this);
         orders = new OrdersTab(this);
         outbox = new OutboxTab(this);

         tabs = new TabBar(new TabBar.Tab("Parts", MaterialIcon.of(IC_PARTS, NAV_ICON)),
            new TabBar.Tab("Suppliers", MaterialIcon.of(IC_SUPPLIERS, NAV_ICON)),
            new TabBar.Tab("Orders", MaterialIcon.of(IC_ORDERS, NAV_ICON)),
            new TabBar.Tab("Outbox", MaterialIcon.of(IC_OUTBOX, NAV_ICON)));
         tabs.setContent(parts, suppliers, orders, outbox);
         // Entering a destination re-reads the store: the user may have edited a part two tabs ago,
         // and a listing that shows what was true when it was last built is a listing that lies.
         tabs.onTabChanged(i -> refreshAll());
         // A TabBar is a tab strip by default — tabs on TOP, the desktop idiom. A phone app wants
         // them at the bottom, within reach of a thumb, and that is one flag on the skin's own
         // style rather than a style of our own: everything else (shape, typography, the tonal pill
         // behind the active icon) keeps coming from the skin.
         tabs.style(Skin.get()
            .tabBar()
            .copy()
            .tabsAtBottom(true));
         tabs.testTag("crud.tabs");
         // Opened from a menu, the app needs a way back to it; as the whole app (the root
         // window) it has none to go to, and each tab already titles itself.
         if (!isRoot())
            add(0f, new AppBar("Depot", this::hide));
         add(LAY_FILL, tabs);

         // The shell was built after onAdd returned, so the layout it already computed is stale.
         setValidLayout(false);
         Screen.repaint();
         updateBadge();
      }
      catch (Exception e)
      {
         dev.crossfly.util.Debug.handle(e);
      }
   }

   /**
    * Re-reads the store into every destination.
    *
    * <p>Called after any write, and when a destination is entered. Blunt on purpose: at this size
    * a full re-read costs less than the machinery needed to work out which screens a given write
    * affected, and it cannot be wrong. Introduce something finer when a measurement, not a
    * suspicion, says it is needed.</p>
    */
   public void refreshAll()
   {
      if (parts == null)
         return;
      parts.reload();
      suppliers.reload();
      orders.reload();
      outbox.reload();
      updateBadge();
      Screen.repaint();
   }

   /** Puts the pending count on the Outbox tab, or clears it when there is nothing waiting. */
   private void updateBadge()
   {
      int n = Repository.get()
         .pendingCount();
      tabs.badge(TAB_OUTBOX, n == 0 ? null : "" + n);
   }

   /**
    * Switches destinations from a child screen.
    *
    * @param index one of the {@code TAB_*} constants
    */
   public void goToTab(int index)
   {
      tabs.setActiveTab(index);
      refreshAll();
   }
}
Para uma IA

Versões desta página legíveis por máquina, para um modelo ler a prateleira em vez de adivinhar a API.

Este exemplo, em JSONEste exemplo, em Markdown
AnteriorReading an odometer (OCR)PróximoPulse — a dashboard