package_context 2.2.0
package_context: ^2.2.0 copied to clipboard
Initialize a package's config and dependencies as one graph, and read them from a typed package context.
package_context #
Holds one config and one dependency graph for a feature package.
The app initializes the package at startup. After that the package reads typed
config and dependencies. The app never imports package_context.

This is not a DI container. Put host values and host objects here. Keep repositories, use cases, and blocs in the package's own container.
| Type | Content | Examples |
|---|---|---|
PackageConfig |
Values from the host | URL, keys, flags |
PackageDependencies |
Objects the package must not create | HTTP client, session, adapters |
PackageGraph |
Config and dependencies together | The only valid initialized state |
Keep one PackageContext per feature package in each isolate. New isolates
have separate global state and must initialize their own context.
An assigned graph makes config and dependencies available. It does not
mean package DI is ready: await ensureInitialized before using registered types.
An empty Config is fine. The instance is still required.
Install #
Add the dependency to the feature package, not to the app:
dependencies:
package_context: ^2.2.0
In a monorepo:
dependencies:
package_context:
path: ../package_context
Wire a feature package #
The samples use a fictional catalog package. The complete
runnable example demonstrates the same
config, dependencies, getters, and bootstrap with an in-memory host.

Layout #
packages/catalog/
lib/catalog.dart
lib/src/config/config.dart
lib/src/dependencies/dependencies.dart
lib/src/core/package_context.dart
lib/src/core/init_package.dart
test/flutter_test_config.dart
Export Config, Dependencies, and initPackage. Do not export packageContext.
export 'src/config/config.dart';
export 'src/core/init_package.dart';
export 'src/dependencies/dependencies.dart';
Config #
Import package_context with an alias. The app resolves flavors and
--dart-define. The package receives ready values.
import 'package:package_context/package_context.dart' as package_context;
final class Config extends package_context.PackageConfig {
final Uri baseUrl;
final bool isEnabled;
const Config({
required this.baseUrl,
required this.isEnabled,
});
}
No values? Keep an empty config:
final class Config extends package_context.PackageConfig {
const Config();
}
Dependencies #
Host infrastructure and host implementations of package interfaces go here. Objects the package constructs itself do not.
import 'package:package_context/package_context.dart' as package_context;
abstract interface class ApiClient {
Future<List<int>> get(Uri url);
}
abstract interface class Session {
String? get userId;
}
final class Dependencies extends package_context.PackageDependencies {
final ApiClient apiClient;
final Session session;
const Dependencies({
required this.apiClient,
required this.session,
});
}
- Interface in the package, implementation in the app →
Dependencies - Host client, retry, clock →
Dependencies - Package-built repository, use case, bloc → package DI
Context #
One context per isolate. Typed getters. Internal code imports the getters, not
package_context.
import 'package:package_context/package_context.dart' as package_context;
final packageContext = package_context.PackageContext<Config, Dependencies>();
Config get config => packageContext.config;
Dependencies get dependencies => packageContext.dependencies;
initPackage #
Call it after the host can build every Dependencies field.
When no binding operation is pending:
| Context | isBound |
Action |
|---|---|---|
| Empty | false |
Set the graph, then bind |
| Empty | true |
Set the graph without binding |
| Initialized | false |
Replace the graph, then bind |
| Initialized | true |
Keep the previous graph without binding |
Do not call initialize twice. Use refresh or ensureInitialized.
Future<void> initPackage({
required Config config,
required Dependencies dependencies,
}) {
return packageContext.ensureInitialized(
graph: package_context.PackageGraph(
config: config,
dependencies: dependencies,
),
isBound: getIt.isRegistered<CatalogFacade>(),
bind: configureDependencies,
);
}
configureDependencies registers package-owned types and can return void or
Future<void>. The registration check must become true only when all package
registration has completed. A partially registered facade is not sufficient.
While binding is pending, external calls with identical config and
dependencies objects receive the same Future. A new PackageGraph wrapper
is allowed; the objects themselves are compared by identity, not equality.
Only the first callback runs, even if a later caller supplies isBound: true.
A different graph or reentry from the pending operation's own bind returns a
failed Future with PackageContextInitializationInProgress. Reentry is rejected
both before and after await, so binding cannot wait on its own completion.
During binding, refresh and reset throw the same error synchronously.
initialize still throws PackageContextAlreadyInitialized.
The graph is available throughout binding. If binding fails, the graph stays
assigned and the original error and stack trace reach callers. The library does
not roll back the host container or retry automatically. The host must clear
partial registrations, then explicitly retry with isBound: false. Await the
result before reading package-owned registrations.

Data layer #
Read the getters. Do not take host objects in constructors.
class CatalogRepository {
const CatalogRepository();
Future<List<int>> fetchItems() {
return dependencies.apiClient.get(config.baseUrl.resolve('/items'));
}
}
UI talks to package DI. Widgets do not read packageContext.
Wire the application #
Import the feature barrel. Do not import package_context.
import 'package:catalog/catalog.dart' as catalog;
Order:
main
→ build host objects
→ init packages that catalog needs
→ catalog.initPackage(config, dependencies)
→ run the app
await catalog.initPackage(
config: catalog.Config(
baseUrl: Uri.parse('https://api.example.com'),
isEnabled: true,
),
dependencies: catalog.Dependencies(
apiClient: appApiClient,
session: AppSession(
auth: auth,
),
),
);
Pass the same host objects you already use. Do not create a second client "for this package".
The interface lives in the feature package. The implementation lives in the app.
class AppSession implements catalog.Session {
final Auth auth;
@override
String? get userId => auth.currentUserId;
AppSession({
required this.auth,
});
}
Tests #
Assign fresh host objects in setUp and call packageContext.reset() in
tearDown. Await pending binding before teardown. Each isolate needs its own
initialization.
Test bootstrap contains the compiled setup with
an in-memory client and session. Bootstrap tests
show the setUp/tearDown wiring and verify repository reads and fresh objects
after reset. Uri.parse runs at runtime, so the containing Config invocation
is not const.
Contract #
| API | Behavior |
|---|---|
isInitialized |
true when a PackageGraph is set; does not report DI readiness |
| getters | PackageContextNotInitialized if empty |
initialize |
Assigns once. PackageContextAlreadyInitialized if already set |
refresh |
Sets or replaces the graph; throws while binding is pending |
ensureInitialized |
Applies the four-state table; shares pending binding for identical host objects |
reset() |
Tests only. Clears the graph; throws while binding is pending |
Pending mutations and conflicting or reentrant initialization use
PackageContextInitializationInProgress. A binding error leaves the graph
assigned and permits explicit retry after host cleanup.
Development #
The supported Dart SDK range remains >=3.13.2 <4.0.0.
CI checks both Dart 3.13.2 and the stable channel on
pull requests to main and pushes to main.
Run make check to resolve dependencies, check formatting without rewriting
source files, analyze, test, and run the main example. make format applies
formatting changes explicitly.
Rules #
- The feature package depends on
package_context. The app does not. - One
packageContextper package in each isolate. - Config holds values. Dependencies hold host objects. Both travel as
PackageGraph. - The public barrel exports
Config,Dependencies,initPackage. - Data and domain read getters. UI does not.
- The app calls
initPackageafter every dependency exists. - Host adapters live in the app.
- Do not read
String.fromEnvironmentinside the package. - Do not call
initializetwice. UserefreshorensureInitialized.
- Repository: github.com/pchkauu/package_context
- Issues: github.com/pchkauu/package_context/issues
- License: MIT