Appearance
Part 3: Isar Repository and Pagination
Pagination is critical for a production expense app because users can easily have thousands of entries.
Repository Interface
dart
abstract class ExpenseRepository {
Future<void> upsertExpense(ExpenseLocal expense);
Future<void> markDeleted(String expenseId);
Future<List<ExpenseLocal>> fetchPage({
required String userId,
DateTime? cursor,
int limit = 20,
});
Stream<List<ExpenseLocal>> watchLatest({
required String userId,
int limit = 20,
});
}Isar Repository Implementation
dart
class IsarExpenseRepository implements ExpenseRepository {
final Isar isar;
IsarExpenseRepository(this.isar);
@override
Future<void> upsertExpense(ExpenseLocal expense) async {
await isar.writeTxn(() async {
await isar.expenseLocals.putById(expense);
});
}
@override
Future<void> markDeleted(String expenseId) async {
final entity = await isar.expenseLocals.filter().idEqualTo(expenseId).findFirst();
if (entity == null) return;
entity
..isDeleted = true
..isSynced = false
..updatedAt = DateTime.now().toUtc();
await isar.writeTxn(() async {
await isar.expenseLocals.put(entity);
});
}
@override
Future<List<ExpenseLocal>> fetchPage({
required String userId,
DateTime? cursor,
int limit = 20,
}) async {
var query = isar.expenseLocals
.filter()
.userIdEqualTo(userId)
.and()
.isDeletedEqualTo(false);
if (cursor != null) {
query = query.and().spentAtLessThan(cursor);
}
return query
.sortBySpentAtDesc()
.limit(limit)
.findAll();
}
@override
Stream<List<ExpenseLocal>> watchLatest({
required String userId,
int limit = 20,
}) {
return isar.expenseLocals
.filter()
.userIdEqualTo(userId)
.and()
.isDeletedEqualTo(false)
.sortBySpentAtDesc()
.limit(limit)
.watch(fireImmediately: true);
}
}Cursor Pagination Strategy
- Keep
cursor = lastItem.spentAtfrom the previous page. - Request next page with
spentAt < cursor. - De-duplicate by
idwhen merging pages in state.