Skip to content

jinyupics/SQLFlutter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 

Repository files navigation

SQLFlutter

UI = sql.f(database) Database = sql.f (Database, UserAction)

State could be misleading, and it should not be too special to manage. We put them in the database and it is de-demonized thereafter.

Routing is Data, Not State

Routing is super important data rather than state. When we want effective data, routing is always in the SQL.

Think of it like Google Sheets formulas:

| A (UserAction) | B (CurrentRoute)          |
|----------------|---------------------------|
| "click_login"  | =IF(A1="click_login", "/login", B0) |
| "submit_form"  | =IF(A2="submit_form", "/dashboard", B1) |

In SQL terms, we simply have a routing table:

CREATE TABLE routing (
  id INTEGER PRIMARY KEY,
  current_screen TEXT NOT NULL
);

-- That's it. One row, one truth.
SELECT current_screen FROM routing WHERE id = 1;
-- Result: '/dashboard'

When a user action happens, we update the table:

UPDATE routing SET current_screen = '/settings' WHERE id = 1;

The UI listens to this table. The screen you're on is just a SELECT away. No navigation stack, no router configuration, no state management - just data in a table.

Just like a spreadsheet cell updates automatically when its dependencies change, your route is a computed value from your data. No imperative navigator.push() - the route is simply what the SQL says it should be.

Widget Content = SQL Query

Once we know where we are (the active path from the routing table), every widget's content is simply a SQL query result.

-- What should the header show?
SELECT title FROM screens WHERE path = '/dashboard';

-- What items are in the list?
SELECT * FROM tasks WHERE user_id = 1 AND completed = 0;

-- What's the badge count?
SELECT COUNT(*) FROM notifications WHERE read = 0;

-- What's the user's name in the avatar?
SELECT name, avatar_url FROM users WHERE id = 1;

Each widget binds to a query. When data changes, the widget updates. No setState(), no notifyListeners(), no rebuilding widget trees manually.

┌─────────────────────────────────────┐
│  Screen: /dashboard                 │  ← SELECT current_screen FROM routing
├─────────────────────────────────────┤
│  Header: "My Tasks"                 │  ← SELECT title FROM screens
├─────────────────────────────────────┤
│  ┌─────────────────────────────┐    │
│  │ □ Buy groceries             │    │  ← SELECT * FROM tasks
│  │ □ Call mom                  │    │
│  │ □ Finish report             │    │
│  └─────────────────────────────┘    │
├─────────────────────────────────────┤
│  [+ Add Task]  Notifications: 3     │  ← SELECT COUNT(*) FROM notifications
└─────────────────────────────────────┘

The entire UI is a reflection of the database. SQL is the single source of truth.

User Actions = Upserts, Not New States

User actions should always be converted to upserting existing or new tables - never to creating new states.

-- User clicks "Add Task" button
INSERT INTO tasks (id, title, user_id, completed)
VALUES (uuid(), 'New task', 1, 0);

-- User toggles a task complete
UPDATE tasks SET completed = 1 WHERE id = 42;

-- User updates their profile (upsert pattern)
INSERT INTO users (id, name, email) VALUES (1, 'John', 'john@email.com')
ON CONFLICT(id) DO UPDATE SET name = excluded.name, email = excluded.email;

-- User navigates to settings
UPDATE routing SET current_screen = '/settings' WHERE id = 1;

Every user interaction is an INSERT, UPDATE, or UPSERT. That's it.

Decoupled Architecture

DB → UI and Action → DB are completely decoupled:

┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│   Action    │ ───►  │  Database   │ ───►  │     UI      │
│  (Upsert)   │       │  (Tables)   │       │  (Queries)  │
└─────────────┘       └─────────────┘       └─────────────┘
                            │
                            ▼
                      Single Source
                       of Truth
  • Action → DB: User actions write to the database (INSERT/UPDATE)
  • DB → UI: UI reads from the database (SELECT)

They don't know about each other. The database is the only connection between them.

This means:

  • Actions don't care how the UI will render
  • UI doesn't care what triggered the data change
  • Testing is trivial: insert data, assert UI; trigger action, assert database
  • Time travel / undo: just restore previous row states

No Service Layer. No Model Layer.

Traditional architecture:

Action → Controller → Service → Repository → Model → Database
                                                         ↓
UI ← ViewModel ← UseCase ← Service ← Repository ← Model ←

SQLFlutter:

Action → Database → UI

That's it. No services. No models. No repositories. No DTOs. No mappers.

  • Tables are your models - the schema defines the shape of your data
  • SQL is your service layer - queries contain the business logic
  • The database is your repository - it already knows how to persist and retrieve

Why add layers that just pass data through? The database already does the job.


Vision: Write Flutter Like You Write Excel

The Problem Today

Traditional Drift requires too many steps:

Step Traditional Drift
1 Define Table classes
2 Run build_runner to generate code
3 Write DAO methods
4 Wrap with StreamBuilder
5 Handle async/await

5 steps just to display one piece of data. Too heavy.

What We Want

Text("Total: ${sql.watch('SELECT COUNT(*) FROM tasks')} tasks")

1 step. Done.

Just like Excel formulas — write what you need, data changes, UI refreshes automatically.

The Ideal API

Method Purpose Reactive
sql.watch() Query data ✓ Auto-refresh
sql.run() Insert/Update/Delete ✗ Execute once

Full Example

class TaskListPage extends StatelessWidget {
  final int userId;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Header
        Text("${sql.watch('SELECT name FROM users WHERE id = $userId')}'s Tasks"),
        Text("Total: ${sql.watch('SELECT COUNT(*) FROM tasks WHERE user_id = $userId')} items"),

        // List
        Expanded(
          child: ListView(
            children: [
              for (var task in sql.watch('SELECT * FROM tasks WHERE user_id = $userId ORDER BY due_date'))
                ListTile(
                  leading: Checkbox(
                    value: task.done == 1,
                    onChanged: (_) => sql.run('UPDATE tasks SET done = 1 - done WHERE id = ${task.id}'),
                  ),
                  title: Text(task.title),
                  subtitle: Text("${task.dueDate}"),
                  onTap: () => sql.run('UPDATE routing SET current_screen = "/task/${task.id}"'),
                ),
            ],
          ),
        ),

        // Add button
        ElevatedButton(
          onPressed: () => sql.run('INSERT INTO tasks (user_id, title) VALUES ($userId, "New Task")'),
          child: Text("Add"),
        ),
      ],
    );
  }
}

Result

┌─────────────────────────────────────────────┐
│  John's Tasks                               │
│  Total: 5 items                             │
├─────────────────────────────────────────────┤
│                                             │
│  ☐ Buy milk                                 │
│    2024-01-15                               │
│                                             │
│  ☑ Call mom                                 │
│    2024-01-14                               │
│                                             │
│  ☐ Finish report                            │
│    2024-01-16                               │
│                                             │
│  ☐ Book dentist                             │
│    2024-01-20                               │
│                                             │
│  ☑ Pay bills                                │
│    2024-01-13                               │
│                                             │
├─────────────────────────────────────────────┤
│              [ + Add ]                      │
└─────────────────────────────────────────────┘

Interactions:

  • Tap ☐ → sql.run(UPDATE...) → Automatically becomes ☑
  • Tap [Add] → sql.run(INSERT...) → List grows, "Total: 6 items"
  • Tap a row → Routing table updates → Navigates to detail page

Everything auto-refreshes. Zero setState. Zero StreamBuilder.

Core Philosophy

SQL queries as simple as reading a variable.

Writing apps as natural as writing Excel.

This is the future SQLFlutter aims to build.

About

UI = sql.f(database) Database = sql.f (Database, UserAction)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors