Skip to content

Part 3: Isar Repository and Pagination

Pagination is critical for a production expense app because users can easily have thousands of entries.

Pagination flow

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.spentAt from the previous page.
  • Request next page with spentAt < cursor.
  • De-duplicate by id when merging pages in state.

Previous: Part 2 - Folder Structure and Models

Next: Part 4 - Riverpod State Management