跳到内容
CrossFly 是 100% 开源在 GitHub 关注我们
CrossFlyCrossFly
开始使用
全部示例
配方数据154 行 · 1 个文件

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.

它展示了什么

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.

在你的机器上运行

每个示例都是普通的 Window,启动器只需要它的名字。

gradlew lwjgl3:run --args="classname dev.cf.recipe.R18Storage"
修改它

Java 是提前编译的,浏览器无法自行重建你的修改 —— 这里的代码在发布前已由 TeaVM 转成 JavaScript。把文件复制到项目里,它在四个平台上原样运行;上面那条命令就是全部准备工作。

正在此处运行CrossFly 0.1.32
正在启动应用…

由 TeaVM 编译为 JavaScript —— 是同一份代码,不是录像。 · 全部示例

它用到了什么

本示例触及的框架类型。带链接的有参考页。

ButtonCardComponentDebugDeviceGapGridLayoutMaterialIconScreenSqliteCursorSqliteDbStylesTextFieldVerticalLayoutWidgets

源码

完整无删减。这就是那个文件,不是片段。

原始
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;
      }
   }
}
给 AI 助手

本页的机器可读版本,让模型直接读取示例集,而不是猜测 API。

本示例(JSON)本示例(Markdown)
上一个A list too big to build下一个Capturing a signature