kiss_repository Usage Guide
A lightweight, generic repository pattern implementation for Dart following the KISS principle.
Core Concepts
Repository
The abstract base class defining all data operations. Generic type T is your domain model.
IdentifiedObject
Wraps an object with its unique string ID:
final item = IdentifiedObject('user-123', User(name: 'Alice'));
// item.id == 'user-123'
// item.object == User(name: 'Alice')
Query System
Extend Query for domain-specific filters. Implement QueryBuilder<T> to translate queries to your implementation.
Available Implementations
- InMemoryRepository: Testing and temporary storage
- JsonFileRepository: File-based JSON persistence
CRUD Operations
Reading Data
// Get single item by ID (throws RepositoryException.notFound if missing)
final user = await repository.get('user-123');
// Query multiple items
final users = await repository.query(query: ActiveUsersQuery());
// Get all items
final allUsers = await repository.query();
Creating Data
// Add with explicit ID
await repository.add(IdentifiedObject('user-123', user));
// Add with auto-generated ID
await repository.addAutoIdentified(
user,
updateObjectWithId: (user, id) => user.copyWith(id: id),
);
Updating Data
// Update with transformer function
final updated = await repository.update('user-123', (current) =>
current.copyWith(name: 'New Name')
);
Deleting Data
await repository.delete('user-123');
Batch Operations
// Add multiple items atomically
await repository.addAll([
IdentifiedObject('id1', item1),
IdentifiedObject('id2', item2),
]);
// Update multiple items
await repository.updateAll([
IdentifiedObject('id1', updatedItem1),
IdentifiedObject('id2', updatedItem2),
]);
// Delete multiple items
await repository.deleteAll(['id1', 'id2', 'id3']);
Streaming (Real-time Updates)
// Stream single item changes
repository.stream('user-123').listen((user) {
print('User updated: ${user.name}');
});
// Stream query results
repository.streamQuery(query: ActiveUsersQuery()).listen((users) {
print('Active users: ${users.length}');
});
Streams emit immediately with current data (BehaviorSubject-like behavior).
Query Implementation
Define Custom Query
class UsersByRoleQuery extends Query {
const UsersByRoleQuery(this.role);
final String role;
}
Implement QueryBuilder
class UserQueryBuilder implements QueryBuilder<InMemoryFilterQuery<User>> {
@override
InMemoryFilterQuery<User> build(Query query) {
if (query is UsersByRoleQuery) {
return InMemoryFilterQuery<User>((user) => user.role == query.role);
}
return InMemoryFilterQuery<User>((user) => true);
}
}
Error Handling
try {
final user = await repository.get('non-existent');
} on RepositoryException catch (e) {
switch (e.code) {
case RepositoryErrorCode.notFound:
// Item doesn't exist
case RepositoryErrorCode.alreadyExists:
// ID already in use (on add)
case RepositoryErrorCode.unknown:
// Other error
}
}
Creating a Repository
InMemoryRepository
final repository = InMemoryRepository<User>(
queryBuilder: UserQueryBuilder(),
path: 'users',
initialItems: [
IdentifiedObject('user-1', User(name: 'Alice')),
],
);
JsonFileRepository
final repository = JsonFileRepository<User>(
queryBuilder: UserQueryBuilder(),
path: 'users',
file: File('users.json'),
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
Resource Management
Always dispose repositories when done:
repository.dispose();
This cleans up all internal streams and resources.
Best Practices
- Generate IDs outside the repository or use
addAutoIdentified - Use
InMemoryRepositoryfor unit tests - Use
JsonFileRepositoryfor integration tests - Always call
dispose()when done - Handle
RepositoryExceptionfor error cases - Use streaming for real-time UI updates
- Use batch operations for multiple items of same type
