Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started
All examples
RecipesData154 lines · 1 files

Storing something

Add a few notes and reopen the recipe — they are still there. The same code runs on desktop, Android, iOS and the web.

What this shows

Storing something, on every platform, with no ORM and no generated code.

SqliteDb is the whole data layer — native on desktop, Android and iOS, and WASM on the web, so the same four calls work everywhere. exec for DDL, execute for a parameterised write, query for a cursor, and transaction to make several writes one thing.

Two habits worth copying from the first line you write. Bind parameters, never concatenate SQL — the ? form is not only safe, it lets SQLite reuse the prepared statement. And name your columns in the SELECT: the cursor reads by position, so a SELECT * silently shifts every getter the day somebody adds a column.

Run it on your machine

Every example is a plain Window, so the launcher only needs its name.

gradlew lwjgl3:run --args="classname dev.cf.recipe.R18Storage"
Editing it

Java is compiled ahead of time, so the browser cannot rebuild an edit on its own — the code you see here was turned into JavaScript by TeaVM before it was published. Copy the file into a project and it runs unchanged on all four platforms; the command above is the whole setup.

Running hereCrossFly 0.1.32
Starting the app…

Compiled to WebAssembly-era JavaScript by TeaVM — the same code, not a video of it. · All examples

What it uses

Framework types this example touches. The linked ones have a reference page.

ButtonCardComponentDebugDeviceGapGridLayoutMaterialIconScreenSqliteCursorSqliteDbStylesTextFieldVerticalLayoutWidgets

The source

Complete and unabridged. This is the file, not an excerpt of it.

Raw
dev/cf/recipe/R18Storage.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.device.Device;
import dev.crossfly.sqlite.SqliteCursor;
import dev.crossfly.sqlite.SqliteDb;
import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * Storing something, on every platform, with no ORM and no generated code.
 *
 * <p>{@link SqliteDb} is the whole data layer — native on desktop, Android and iOS, and WASM on the
 * web, so the same four calls work everywhere. {@code exec} for DDL, {@code execute} for a
 * parameterised write, {@code query} for a cursor, and {@code transaction} to make several writes
 * one thing.</p>
 *
 * <p>Two habits worth copying from the first line you write. <b>Bind parameters, never concatenate
 * SQL</b> — the {@code ?} form is not only safe, it lets SQLite reuse the prepared statement.
 * And <b>name your columns in the SELECT</b>: the cursor reads by position, so a {@code SELECT *}
 * silently shifts every getter the day somebody adds a column.</p>
 */
public class R18Storage extends Recipe
{
   private SqliteDb db;
   private TextField input;
   private Component list;

   public String title()
   {
      return "Storing something";
   }

   public String note()
   {
      return "Add a few notes and reopen the recipe — they are still there. The same code runs on"
         + " desktop, Android, iOS and the web.";
   }

   public void build(Component body) throws Exception
   {
      open();

      Component row = new Component();
      row.setLayout(new GridLayout(Position.START, Position.CENTER));
      input = new TextField();
      input.caption("A note");
      input.testTag("store.input");
      Button add = new Button(MaterialIcon.of(0xE145, 1.4f));   // add
      add.getStyle()
         .backColor(Tones.accent())
         .iconColor(Styles.colorScheme().onPrimary)
         .borderColor(0);
      add.testTag("store.add");
      add.onAction(c -> save());
      row.add(1f, 0f, input, outgap(add, .5f, 0, 0, 0));
      body.add(LAY_AUTO, outgap(row, 0, 0, 0, .8f));

      list = new Component();
      list.setLayout(new VerticalLayout(Position.START, Position.START));
      list.testTag("store.list");
      body.add(LAY_AUTO, list);
      reload();
   }

   private void open()
   {
      String base = Device.homeFolder;
      db = SqliteDb.open(base == null || base.isEmpty() ? "cf-notes.db" : base + "/cf-notes.db");
      db.exec("CREATE TABLE IF NOT EXISTS note(id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)");
   }

   private void save()
   {
      String t = input.getText()
         .trim();
      if (t.isEmpty())
         return;
      db.execute("INSERT INTO note(text) VALUES(?)", t);   // bound, never concatenated
      input.setText("");
      reload();
      Screen.repaint();
   }

   private void reload()
   {
      try
      {
         list.removeAll();
         // removeAll() takes the layout with it — put it back before adding anything.
         list.setLayout(new VerticalLayout(Position.START, Position.START));
         // Columns named, so position 0 stays position 0 when the table grows a column.
         try (SqliteCursor c = db.query("SELECT id, text FROM note ORDER BY id DESC LIMIT 12"))
         {
            boolean any = false;
            while (c.next())
            {
               any = true;
               list.add(LAY_AUTO, note(c.getLong(0), c.getString(1)));
            }
            if (!any)
               list.add(LAY_AUTO, label("Nothing stored yet.", 1f, false, Styles.captionColor,
                  Position.START));
         }
         // invalidateMeasure, not setValidLayout(false): the list grew, and the scrolling body
         // around it has to measure it again - or the new note came out cut (iPhone, 17/10/2026).
         list.invalidateMeasure();
      }
      catch (Exception e)
      {
         dev.crossfly.util.Debug.handle(e);
      }
   }

   private Component note(final long id, String text) throws Exception
   {
      Card c = new Card();
      c.setLayout(new GridLayout(Position.START, Position.CENTER));
      c.getStyle()
         .outgap(Gap.ofFont(0, 0, 0, .4f));
      Button del = new Button(MaterialIcon.of(0xE872, 1.2f));   // delete
      del.getStyle()
         .iconColor(Tones.danger())
         .borderColor(0)
         .backColor(0);
      del.onAction(e ->
      {
         db.execute("DELETE FROM note WHERE id=?", id);
         reload();
         Screen.repaint();
      });
      c.add(1f, 0f, label(text, 1f, false, Styles.textColor, Position.START), del);
      return c;
   }

   /** Closing when the screen goes away — a connection is a resource, not a value. */
   @Override
   public void onRemove()
   {
      super.onRemove();
      if (db != null)
      {
         db.close();
         db = null;
      }
   }
}
For an AI assistant

Machine-readable versions of this page, so a model can read the shelf instead of guessing at the API.

This example, as JSONThis example, as Markdown
PreviousA list too big to buildNextCapturing a signature