Skip to content
← All writing
March 11, 2026·3 min read

Storing Data in Flutter with SQLite

Most Flutter apps eventually need to store real, structured data on the device — a list of expenses, saved records, offline content. For anything beyond a few simple values, SQLite is the standard answer: a fast, reliable, embedded database that ships inside the app with no server required. This post covers when to reach for it and how to use it well.

First: do you even need SQLite?

Not all local storage needs a database. Match the tool to the data:

  • A few simple values (a theme setting, a flag, a token)? Use lightweight key-value storage. A database is overkill.
  • Structured, queryable, related data (many records you need to filter, sort, or relate)? That's SQLite's job.

Reaching for SQLite to store one boolean is over-engineering; storing hundreds of related records in a key-value store is under-engineering. Pick based on the shape of your data.

Why SQLite fits mobile so well

  • Embedded and offline. The database lives in a file on the device. No network, no server, works on a plane.
  • Structured queries. You get real SQL — filtering, sorting, aggregating — instead of hand-rolling logic over a blob of JSON.
  • Fast and battle-tested. SQLite is one of the most widely deployed pieces of software in the world; it's dependable.
  • Scales to real data. Thousands of rows are no problem, and queries stay fast with proper indexing.

The shape of the code

You define tables, then insert and query rows. Conceptually:

// create a table (run once, on database creation)
await db.execute('''
  CREATE TABLE expenses(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    amount REAL NOT NULL,
    created_at TEXT NOT NULL
  )
''');

// insert
await db.insert('expenses', expense.toMap());

// query
final rows = await db.query('expenses', orderBy: 'created_at DESC');
final expenses = rows.map(Expense.fromMap).toList();

The important habit here is the toMap/fromMap pair: convert between your Dart model objects and the database rows in one place, so the rest of your app works with clean typed objects, not raw maps.

Plan for schema changes

The mistake that bites everyone eventually: you ship version 1, then later need a new column. Existing users already have the old database file on their device. If you don't handle this, the app crashes for them.

SQLite libraries provide a version number and migration hooks for exactly this. Every time you change the schema, bump the version and write a migration that upgrades old databases to the new shape. Treat migrations as append-only history — never edit a shipped migration, always add a new one. Getting this discipline right from day one saves painful production incidents later.

Keep the database layer separate

Don't scatter SQL across your widgets. Put all database code behind a small repository or data access layer — a class whose methods are things like getExpenses(), addExpense(), deleteExpense(). Benefits:

  • Your UI works with meaningful methods, not SQL strings.
  • If you ever change storage engines, you change one layer, not the whole app.
  • The data logic becomes testable in isolation.

Practical tips

  • Index columns you query or sort by. An index turns a slow scan into a fast lookup as your data grows.
  • Do writes off the UI thread. Database work is I/O; keep it async so the interface stays smooth.
  • Use transactions for bulk work. Wrapping many inserts in a single transaction is dramatically faster than inserting one at a time.
  • Consider a typed wrapper. Higher-level libraries can generate type-safe queries and handle a lot of the boilerplate — worth it as complexity grows.

Summary

SQLite is the right tool when a Flutter app needs structured, queryable, offline data — but not for a handful of simple settings, where key-value storage is simpler. Map cleanly between your models and rows, keep all the database code behind a repository layer, index what you query, and — most importantly — plan for schema migrations from the start. Do that and you get a fast, dependable local store that grows with your app instead of fighting it.