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
Class
Knows about
Does
DataSource
Network only
Raw HTTP calls, returns Response
Local
sqflite only
CRUD + freshness check via cached_at
BaseStore
Network errors, cache logic
Error wrapping, cache orchestration
Store
DataSource + Local
Extends 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';
}