repository 4.0.0
repository: ^4.0.0 copied to clipboard
A reactive repository toolkit for Flutter with HTTP, caching, dependency tracking, auto refresh, and UI integration.
Repository #
Git hooks #
The hooks require FVM
and use the Flutter version pinned in .fvmrc. Install that SDK and enable the
repository hooks once per worktree:
fvm install
git config extensions.worktreeConfig true
git config --worktree core.hooksPath .githooks
Before each push, the pre-push hook runs fvm flutter analyze and checks
formatting with fvm dart format. The push is blocked if either check fails.
A reactive repository toolkit for Flutter with HTTP, caching, dependency tracking, auto refresh, and UI integration.
Warning ⚠️ #
This package is currently under heavy development and is not yet ready for production use. It is being actively worked on by our development team, and we are constantly adding new features and making improvements.
While we are working hard to make this package as stable and reliable as possible, there may be bugs or issues that arise as new code is added or existing code is modified. We encourage you to report any issues you encounter during this development process, and we will do our best to address them as quickly as possible.
As we continue to develop this package, we may make breaking changes to the API or other aspects of the package. We will do our best to document any such changes and provide guidance on how to update your code accordingly.
We appreciate your patience and understanding as we work to bring this package to maturity. We are committed to delivering a high-quality, reliable package that meets the needs of our users, and we believe that with your feedback and support, we can achieve that goal.
Installation 💻 #
❗ Repository now includes its Flutter integration, so a Flutter SDK is required.
Add repository to your pubspec.yaml:
dependencies:
repository: ^4.0.0-dev.1
Hive persistence is provided by the optional adapter package:
dependencies:
repository: ^4.0.0-dev.1
repository_cache_hive: ^4.0.0-dev.1
Install it:
flutter pub get
Configure repositories #
When using Hive, import its adapter separately:
import 'package:repository/repository.dart';
import 'package:repository_cache_hive/repository_cache_hive.dart';
Configure the default client and the session lifecycle once before creating repositories:
final repositories = BaseRepository.config(
baseUrl: Uri.parse('https://api.example.com/v1/'),
httpClient: createPlatformHttpClient(),
storage: await HiveRepositoryCacheStorage.create(),
sessionManager: .bearer(
storage: secureSessionStorage,
refresh: .post(
endpoint: .relative('/auth/refresh'),
body: (input) => {
'refresh_token': input.session.refreshToken,
},
decode: (json, current) => .new(
accessToken: json['access_token']! as String,
refreshToken:
json['refresh_token'] as String? ?? current.refreshToken,
),
),
authentication: (auth) => (
password: auth.password(
endpoint: .relative('/auth/login'),
body: (input) => {
'email': input.username,
'password': input.password,
},
decode: (json) => .new(
accessToken: json['access_token']! as String,
refreshToken: json['refresh_token'] as String?,
),
),
),
),
);
await repositories.auth.password.signIn(
username: email,
password: password,
);
The callback returned by authentication remains fully typed. In a bearer
manager every terminal decoder must return RepositorySessionBearer; in a
cookies manager it must return RepositorySessionCookies. Authentication
endpoints do not share a default decoder because each backend response may
have a different shape.
RepositorySessionStorage is intentionally separate from repository cache
storage. Use a secure platform-backed implementation for persisted access and
refresh tokens. InMemoryRepositorySessionStorage is available for tests and
sessions that should not survive an application restart.
Each repository captures the configured client when it is created. A later
configuration does not change existing repositories. client: remains an
optional per-repository override for tests or different infrastructure.
createPlatformHttpClient() owns its internally created transport. To inject
a package:http client, pass client:; it remains caller-owned by default, or
set closeClient: true to transfer ownership to the adapter. Call close() on
the adapter, or on a directly held RepositoryClient, during application
shutdown.
A read-only repository needs only an endpoint, decoder, and empty actions factory:
final repository = Repository<int, NoRepositoryActions>(
endpoint: .relative('count'),
fromJson: int.parse,
actions: (_) => (),
);
NoRepositoryActions is the empty record used by read-only repositories.
Request URLs #
Relative URLs are resolved against RepositoryClient.baseUrl before any
interceptor runs:
RepositoryHttpRequest(
url: .relative('transactions/42'),
);
An absolute URL ignores the configured base URL:
RepositoryHttpRequest(
url: .absolute('https://external.example.com/transactions/42'),
);
Resolution follows Uri.resolve: a leading slash starts at the host root,
while a path without one is relative to the base URL path.
Interceptors #
RepositoryInterceptor is middleware around the configured HTTP adapter.
Interceptors run in declaration order and may transform requests, transform
responses, handle failures, short-circuit the transport, or invoke next
again. The latter enables session refresh followed by a single replay.
class HeaderInterceptor implements RepositoryInterceptor {
const HeaderInterceptor();
@override
Future<RepositoryHttpResponse> intercept({
required RepositoryHttpRequest request,
required RepositoryRequestHandler next,
}) {
return next(
RepositoryHttpRequest(
url: request.url,
method: request.method,
body: request.body,
headers: {...request.headers, 'X-App': 'example'},
),
);
}
}
When a session manager is configured, its interceptor is installed automatically after the declared application interceptors. Token resolution and refresh are single-flight, and an unauthorized request is replayed at most once.
Repository access #
Repositories use authenticated access by default. Without a session they stay pending and do not call their endpoint. Login activates existing authenticated repositories; logout clears their in-memory data and scoped cache.
final profile = Repository<Profile, NoRepositoryActions>(
endpoint: .relative('/profile'),
fromJson: Profile.fromJson,
actions: (_) => (),
);
final appVersion = Repository<AppVersion, NoRepositoryActions>(
endpoint: .relative('/version'),
access: .unauthenticated,
fromJson: AppVersion.fromJson,
actions: (_) => (),
);
Remote logout is optional. signOut() always removes local credentials and
authenticated repository data, even if a configured logout endpoint fails:
await repositories.session.signOut();
Cookie sessions use the same typed authentication methods and a user-supplied
RepositoryCookieJar:
sessionManager: .cookies(
cookieJar: cookieJar,
authentication: (auth) => (
password: auth.password(
endpoint: .relative('/auth/login'),
body: (input) => {
'email': input.username,
'password': input.password,
},
decode: (json) => .new(identity: json['user_id'] as String?),
),
),
),
Actions #
Actions are typed Dart functions grouped in a named record. Their factory is
defined outside the repository and receives a RepositoryActionExecutor.
Each run receives the repository's captured RepositoryClient and returns
Either<Failure, Output> from dartz.
typedef TransactionActions = ({
Future<Either<CreateFailure, Transaction>> Function(
CreateTransaction input,
) create,
});
TransactionActions transactionActions(
RepositoryActionExecutor<List<Transaction>> execute,
) {
return (
create: (input) => execute(
run: (client) async {
final response = await client.call(
request: RepositoryHttpRequest(
url: .relative('transactions'),
method: RepositoryHttpMethod.post,
body: input.toJson(),
),
);
if (response.statusCode != 201) {
return Left(CreateFailure.fromResponse(response));
}
return Right(Transaction.fromJson(response.body));
},
update: (current, created) => [...?current, created],
),
);
}
final transactions = Repository<List<Transaction>, TransactionActions>(
endpoint: .relative('transactions'),
fromJson: Transaction.listFromJson,
actions: transactionActions,
);
Left is returned without changing repository data. On Right, the optional
update callback receives the current data and successful output, emits the
new optimistic state, and the same Right is returned to the caller.
Unexpected exceptions propagate and also skip update.
Flutter #
RepositoryBuilder infers both the data and actions types from the repository:
RepositoryBuilder(
repository: transactions,
builder: (context, state, actions) {
return switch (state) {
RepositoryStatePending() => const CircularProgressIndicator(),
RepositoryStateReady(data: final transactions, source: final source) =>
ElevatedButton(
onPressed: () async {
final result = await actions.create(input);
result.fold(showCreateError, showCreatedTransaction);
},
child: Text('Create transaction (${transactions.length}, $source)'),
),
RepositoryStateError(error: final error) => Text('Error: $error'),
};
},
);
The builder receives the complete RepositoryState<Data>, so the UI chooses
how to render pending content or ready data and retains metadata such as the
data source. Request activity is independent from content availability. The
actions record remains inferred from the repository type.
Migration from the monostate API #
- Call
BaseRepository.config(...)once before creating repositories. - Remove repeated
client:arguments; retain them only for explicit overrides. - Replace
Urirequest values with.relative(...)or.absolute(...). - Move action records into external factories that receive
RepositoryActionExecutor<Data>. - Return
Either<Failure, Output>from every actionrun. - Use
Repository<Data, NoRepositoryActions>withactions: (_) => ()for repositories without custom actions. - The second
RepositoryBuildercallback argument is the completeRepositoryState<Data>instead of nullable data. The third argument is the typed actions container instead of the repository. - Remove session mixins and repeated session dependencies. Configure a
RepositorySessionManageronce and mark only public repositories withaccess: .unauthenticated.
Continuous Integration 🤖 #
Repository comes with a built-in GitHub Actions workflow powered by Very Good Workflows but you can also add your preferred CI/CD solution.
Out of the box, on each pull request and push, the CI formats, lints, and tests the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses Very Good Analysis for a strict set of analysis options used by our team. Code coverage is enforced using the Very Good Workflows.
Running Tests 🧪 #
The packages are managed as a Pub Workspace orchestrated by Melos. Install the workspace dependencies and run all quality checks with:
flutter pub get
dart run melos run quality
Individual workspace commands are also available:
dart run melos run format
dart run melos run analyze
dart run melos run test
To run only the main package tests with coverage:
flutter test --coverage
To view the generated coverage report you can use lcov.
# Generate Coverage Report
genhtml coverage/lcov.info -o coverage/
# Open Coverage Report
open coverage/index.html