Service Providers¶
Introduction¶
Service providers are the central place of all Bond application bootstrapping. Your own application, as well as all of Bond's core services, are bootstrapped via service providers.
But what do we mean by "bootstrapped"? In general, we mean registering things, including registering service container bindings, event listeners, middleware, and even routes. Service providers are the central place to configure your application.
If you open the lib/app.dart file included with Bond, you will see a providers array. These are all of the service provider classes that will be loaded for your application. By default, a set of Bond core service providers are listed in this array. These providers bootstrap the Bond core components, such as the networking client, cache, form validation, and others.
In this overview, you will learn how to write your own service providers and register them with your Bond application.
Note
Service providers are a great way to group related functionality and keep your application organized. If you find yourself registering many bindings in a single place, consider breaking them up into multiple service providers.
Writing Service Providers¶
All service providers extend the ServiceProvider class. Most service providers contain a register method. Within this method, you should only bind things into the service container. You should never attempt to register any event listeners, routes, or any other piece of functionality within the register method.
The Bond CLI can generate a new provider via the bond create provider command:
The Register Method¶
As mentioned previously, within the register method, you should only bind things into the service container. For example, let's write a service provider that registers an RSS feed reader service:
import 'package:get_it/get_it.dart';
import 'package:bond_core/bond_core.dart';
class RssServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
it.registerLazySingleton<RssReader>(() => RssReader());
}
}
This service provider only defines a register method, and uses that method to define an implementation of RssReader in the service container. If you're not yet familiar with Bond's service container, it's based on GetIt and provides dependency injection throughout your application.
Bindings and Singletons¶
Within a service provider, you always have access to the service container via the it parameter. You can register a binding using the registerFactory method. This method accepts the class or interface name that you wish to register along with a closure that returns an instance of the class:
You may also use the registerLazySingleton method to register a class binding that should only be resolved one time. Once a singleton binding has been resolved, the same object instance will be returned on subsequent calls into the container:
Binding Interfaces to Implementations¶
A very powerful feature of service providers is their ability to bind interfaces to implementations. For example, let's assume we have an EventPusher interface and a RedisEventPusher implementation. Once we have coded our RedisEventPusher implementation of this interface, we can register it with the service provider like so:
This statement tells the container that it should inject the RedisEventPusher when a class needs an implementation of EventPusher. Now we can type-hint the EventPusher interface in the constructor of a class that is resolved by the container:
class OrderController {
final EventPusher pusher;
OrderController(this.pusher);
void store(Order order) {
// Store order...
pusher.push('order.created', order);
}
}
Response Decoding¶
Bond service providers also support response decoding through the ResponseDecoding mixin. This allows you to centralize JSON-to-model conversion logic that can be shared across networking and caching layers:
class UserServiceProvider extends ServiceProvider with ResponseDecoding {
@override
Future<void> register(GetIt it) async {
it.registerLazySingleton(() => UserRepository(it()));
it.registerLazySingleton(() => UserApiService(it()));
}
@override
Map<Type, JsonFactory> get factories => {
User: (json) => User.fromJson(json),
UserProfile: (json) => UserProfile.fromJson(json),
UserPreferences: (json) => UserPreferences.fromJson(json),
};
}
These factories will be automatically used by Bond's networking and caching systems when converting JSON responses to your model objects.
Registering Providers¶
All service providers are registered in the lib/app.dart configuration file. This file contains a providers array where you can list the class names of your service providers. By default, several Bond core service providers are listed. You are free to add your own providers to this list:
final List<ServiceProvider> providers = [
// Bond Core Service Providers
FirebaseServiceProvider(),
AppServiceProvider(),
// Feature Service Providers
AuthServiceProvider(),
UserServiceProvider(),
PostServiceProvider(),
NotificationServiceProvider(),
// Third-party Service Providers
AnalyticsServiceProvider(),
];
Real-World Example: Building an Authentication Service Provider¶
Let's walk through building a complete authentication service provider that demonstrates best practices:
class AuthServiceProvider extends ServiceProvider with ResponseDecoding {
@override
Future<void> register(GetIt it) async {
// Register the HTTP client for auth endpoints
it.registerLazySingleton<AuthApiService>(() => AuthApiService(it()));
// Register the token storage
it.registerLazySingleton<TokenStorage>(() => SecureTokenStorage());
// Register the authentication repository
it.registerLazySingleton<AuthRepository>(() => AuthRepository(
apiService: it<AuthApiService>(),
tokenStorage: it<TokenStorage>(),
));
// Register form controllers
it.registerFactory(() => LoginFormController(it()));
it.registerFactory(() => RegisterFormController(it()));
// Register the auth guard for protected routes
it.registerLazySingleton<AuthGuard>(() => AuthGuard(it()));
}
@override
Map<Type, JsonFactory> get factories => {
User: (json) => User.fromJson(json),
AuthToken: (json) => AuthToken.fromJson(json),
LoginResponse: (json) => LoginResponse.fromJson(json),
RegisterResponse: (json) => RegisterResponse.fromJson(json),
};
}
This provider demonstrates several important concepts:
- Dependency Injection: Each service receives its dependencies through the constructor
- Interface Segregation: Using abstract classes like
TokenStorageallows for easy testing - Factory vs Singleton: Controllers are factories (new instance per request), while repositories are singletons
- Response Decoding: All auth-related models are registered for automatic JSON conversion
Using the Authentication Services¶
Once registered, you can use these services throughout your application:
class LoginPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authRepo = GetIt.instance<AuthRepository>();
final loginController = GetIt.instance<LoginFormController>();
return Scaffold(
body: LoginForm(
controller: loginController,
onSubmit: (email, password) async {
try {
await authRepo.login(email, password);
// Navigate to home
} catch (e) {
// Handle error
}
},
),
);
}
}
Testing Service Providers¶
Service providers make testing much easier by allowing you to swap implementations:
void main() {
group('AuthRepository Tests', () {
late GetIt testContainer;
setUp(() {
testContainer = GetIt.instance;
// Register test implementations
testContainer.registerLazySingleton<AuthApiService>(
() => MockAuthApiService(),
);
testContainer.registerLazySingleton<TokenStorage>(
() => InMemoryTokenStorage(),
);
testContainer.registerLazySingleton<AuthRepository>(
() => AuthRepository(
apiService: testContainer<AuthApiService>(),
tokenStorage: testContainer<TokenStorage>(),
),
);
});
tearDown(() {
testContainer.reset();
});
test('should login successfully with valid credentials', () async {
final authRepo = testContainer<AuthRepository>();
final result = await authRepo.login('test@example.com', 'password');
expect(result.isSuccess, true);
});
});
}
Advanced Patterns¶
Feature-Based Organization¶
Organize your providers by feature to maintain clear boundaries:
lib/
features/
auth/
auth_service_provider.dart
data/
repositories/
api/
presentation/
controllers/
pages/
posts/
post_service_provider.dart
data/
presentation/
notifications/
notification_service_provider.dart
Conditional Registration¶
Sometimes you may want to register services conditionally:
class ApiServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
if (Environment.isDevelopment) {
it.registerLazySingleton<ApiClient>(() => MockApiClient());
} else {
it.registerLazySingleton<ApiClient>(() => HttpApiClient());
}
}
}
Third-Party Integration¶
For third-party services, create dedicated providers:
class FirebaseServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
// Initialize Firebase
final app = await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
it.registerSingleton<FirebaseApp>(app);
it.registerLazySingleton(() => FirebaseAuth.instance);
it.registerLazySingleton(() => FirebaseFirestore.instance);
it.registerLazySingleton(() => FirebaseMessaging.instance);
}
}
Common Pitfalls¶
Avoid Heavy Operations in Register¶
Don't perform heavy operations in the register method:
// ❌ Don't do this
class BadServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
final data = await heavyNetworkCall(); // Bad!
it.registerSingleton(data);
}
}
// ✅ Do this instead
class GoodServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
it.registerLazySingleton(() async {
return await heavyNetworkCall(); // Lazy loading
});
}
}
Don't Create Circular Dependencies¶
// ❌ This creates a circular dependency
class ServiceA {
ServiceA(ServiceB serviceB);
}
class ServiceB {
ServiceB(ServiceA serviceA); // Circular!
}
Keep Providers Focused¶
Each provider should have a single responsibility:
// ❌ Too many responsibilities
class MegaServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
// Auth services
it.registerLazySingleton(() => AuthService());
// Database services
it.registerLazySingleton(() => DatabaseService());
// Analytics services
it.registerLazySingleton(() => AnalyticsService());
// ... 50 more registrations
}
}
// ✅ Focused providers
class AuthServiceProvider extends ServiceProvider { /* auth only */ }
class DatabaseServiceProvider extends ServiceProvider { /* database only */ }
class AnalyticsServiceProvider extends ServiceProvider { /* analytics only */ }
Next Steps¶
Now that you understand service providers, you're ready to explore:
- Modules and Boundaries - Learn how to organize features
- Configuration - Understand environment management
- Data and Networking - Build your first API integration