Keyboard Shortcuts

j / kScroll down / up
ggScroll to top
GScroll to bottom
/Open search
?Show this help
EscClose search / help
n / NNext / previous section
hGo to landing
dGo to docs
:Command mode — type section name

Press ? or Esc to close

Deep Dives

Data Layer

Right-sized for mobile. No repository/usecase ceremony. One Store per feature orchestrates everything: network, local DB, and cache freshness.

sqflite is opt-in. Do not add local DB by default. Add it only per feature that genuinely needs offline support. For v1, skip it entirely and rely on in-memory Cubit state.

Responsibilities

ClassKnows aboutDoes
DataSourceNetwork onlyRaw HTTP calls, returns Response
Localsqflite onlyCRUD + freshness check via cached_at
BaseStoreNetwork errors, cache logicError wrapping, cache orchestration
StoreDataSource + LocalExtends BaseStore, decides strategy

DataSource — plain HTTP caller

// No base class needed. No try/catch. No parsing. Just HTTP.
class ProfileDataSource {
  final ApiClient client;
  ProfileDataSource(this.client);

  Future getProfile() => client.get('/profile');
  Future updateAvatar(String url) =>
      client.patch('/profile', data: {'avatar': url});
}

Local — CRUD + freshness

Future getIfFresh({
  Duration maxAge = const Duration(hours: 1),
}) async {
  final rows = await db.query(ProfileTable.table, limit: 1);
  if (rows.isEmpty) return null;

  final cachedAt = rows.first[ProfileTable.cachedAt] as int;
  final age = DateTime.now().millisecondsSinceEpoch - cachedAt;
  if (age > maxAge.inMilliseconds) return null; // stale

  return ProfileModel.fromMap(rows.first);
}

Future upsert(ProfileModel model) async {
  await db.insert(ProfileTable.table, {
    ...model.toMap(),
    ProfileTable.cachedAt: DateTime.now().millisecondsSinceEpoch,
  }, conflictAlgorithm: ConflictAlgorithm.replace);
}

Migration system

abstract class Migration {
  int get version;
  Future up(Database db);
}

class AppMigrations {
  static final all = [
    CreateProfileTable(),  // v1
    CreateFeedTable(),     // v2
    AddBioToProfile(),     // v3
  ];
  static int get latest => all.last.version;
}

// Feature table — column constants + migration class
class CreateProfileTable extends Migration {
  @override int get version => 1;

  @override
  Future up(Database db) async {
    await db.execute('''
      CREATE TABLE profiles (
        id        TEXT    PRIMARY KEY,
        name      TEXT    NOT NULL,
        avatar    TEXT,
        bio       TEXT,
        cached_at INTEGER NOT NULL
      )
    ''');
  }

  // column constants — never raw strings in queries
  static const table    = 'profiles';
  static const cachedAt = 'cached_at';
}

Cache layer summary

LayerToolPurpose
In-memoryCubit stateSession cache — instant, free, always present
Persistentsqflite (opt-in)Offline-safe, survives restart
Freshnesscached_at columnStaleness detection per table
PreferencesSharedPrefsPrimitives only: locale, theme, flags
sam's arch — A Pragmatic Flutter Architecture