Flutter Widget Previews
This guide shows you how to use get_it with Flutter's widget previewer.
The Challenge
When you use the @Preview annotation, Flutter renders your widget without calling main() or running your app's startup code. This means:
get_ithasn't been initialized- No services have been registered
- Widgets that call
GetIt.I<SomeService>()will throw errors
You need to handle get_it initialization within the preview itself.
Two Approaches
There are two ways to initialize get_it for previews, each with different trade-offs:
- Direct Registration - Simple check and register pattern
- Wrapper Widget - Reusable wrapper with automatic cleanup
Choose based on your needs:
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Direct Registration | Simple, one-off previews | Maximum control, minimal code | No automatic cleanup, manual guards |
| Wrapper Widget | Reusable setups, multiple previews | Automatic cleanup, DRY principle | Slightly more setup |
Approach 1: Direct Registration
The simplest approach is to check if services are registered and register them if not.
How It Works
Flutter may call your preview function multiple times (on hot reload, etc.), so you guard against double registration using isRegistered():
@Preview()
Widget userProfilePreview() {
// The preview function may be called multiple times during hot reload,
// so guard against double registration by checking the last service
if (!getIt.isRegistered<MockApiClient>()) {
getIt.registerSingleton<MockUserService>(MockUserService());
getIt.registerLazySingleton<MockApiClient>(() => MockApiClient());
}
return const MaterialApp(
home: Scaffold(
body: UserProfileWidget(),
),
);
}When to Use
- One-off previews with unique dependencies
- Quick prototyping where you want immediate results
- Maximum control over initialization timing
Pros & Cons
Pros:
- Minimal code, easy to understand
- Full control over registration order
- No additional widgets needed
Cons:
- Manual guard checks for every service
- No automatic cleanup (stays in
get_ituntil manually reset) - Code duplication if multiple previews need same setup
Approach 2: Wrapper Widget
For better organization and reusability, create a wrapper widget that handles initialization and cleanup automatically.
The Wrapper Widget
First, create a reusable wrapper widget:
/// A wrapper widget for Flutter widget previews that initializes GetIt.
///
/// This widget handles GetIt setup in initState and cleanup via reset()
/// in dispose, making it perfect for preview scenarios where widgets are
/// rendered in isolation.
class GetItPreviewWrapper extends StatefulWidget {
const GetItPreviewWrapper({
super.key,
required this.init,
required this.child,
});
/// The child widget to render after GetIt is initialized
final Widget child;
/// Initialization function that registers dependencies in GetIt
final void Function(GetIt getIt) init;
@override
State<GetItPreviewWrapper> createState() => _GetItPreviewWrapperState();
}
class _GetItPreviewWrapperState extends State<GetItPreviewWrapper> {
@override
void initState() {
super.initState();
// Initialize GetIt with preview dependencies
widget.init(GetIt.instance);
}
@override
void dispose() {
// Clean up all GetIt registrations when preview is disposed
GetIt.instance.reset();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}Using the Wrapper
Use the wrapper with the @Preview annotation's wrapper parameter:
// Top-level wrapper function for @Preview annotation
Widget myPreviewWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
// Register all preview dependencies here
getIt.registerLazySingleton<MockApiClient>(() => MockApiClient());
getIt.registerSingleton<MockUserService>(MockUserService());
getIt.registerFactory<MockAuthService>(() => MockAuthService());
},
child: child,
);
}
// Use the wrapper in your preview
@Preview(name: 'Dashboard Widget', wrapper: myPreviewWrapper)
Widget dashboardPreview() => const MaterialApp(
home: Scaffold(
body: DashboardWidget(),
),
);When to Use
- Multiple previews that share the same dependencies
- Reusable setups across different widget previews
- Automatic cleanup via
reset()on dispose - Cleaner code with separation of concerns
Pros & Cons
Pros:
- Automatic cleanup when preview is disposed
- Reusable across multiple previews
- Cleaner preview code (setup is separate)
- Easy to create multiple configurations
Cons:
- Requires separate wrapper widget definition
- Wrapper function must be top-level or static
- Slightly more initial setup
Testing Different Scenarios
One powerful use of the wrapper approach is creating different scenarios for the same widget:
// Create different wrappers for different scenarios
// Logged in user scenario
Widget loggedInWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
getIt.registerSingleton<MockAuthService>(
MockAuthService()..isAuthenticated = true,
);
getIt.registerSingleton<MockUserService>(
MockUserService()..currentUser = 'John Doe',
);
},
child: child,
);
}
// Logged out user scenario
Widget loggedOutWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
getIt.registerSingleton<MockAuthService>(
MockAuthService()..isAuthenticated = false,
);
},
child: child,
);
}
// Error state scenario
Widget errorStateWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
getIt.registerSingleton<MockApiClient>(
MockApiClient()..shouldFail = true,
);
},
child: child,
);
}
// Use different wrappers to preview different states
@Preview(name: 'Login Button - Logged In', wrapper: loggedInWrapper)
Widget loginButtonLoggedIn() => const LoginButtonWidget();
@Preview(name: 'Login Button - Logged Out', wrapper: loggedOutWrapper)
Widget loginButtonLoggedOut() => const LoginButtonWidget();
@Preview(name: 'Dashboard - Error State', wrapper: errorStateWrapper)
Widget dashboardError() => const DashboardWidget();This pattern is excellent for:
- Testing edge cases (error states, empty data, loading)
- Different user states (logged in, logged out, guest)
- Accessibility testing (different font sizes, themes)
- Responsive design (different screen sizes with
sizeparameter)
Complete Example
See the get_it example app for a complete working example showing both approaches.
The example includes:
preview()- Direct registration approachpreviewWithWrapper()- Wrapper approachGetItPreviewWrapperimplementation in preview_wrapper.dart
Tips & Best Practices
Using Real vs Mock Services
One of the key benefits of get_it is that you can connect your widgets to real services in previews, allowing you to see your widgets with actual data and behavior. You just need to ensure proper initialization:
// Real services - perfectly valid if properly initialized
Widget realServicesWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
getIt.registerSingleton<ApiClient>(ApiClient(baseUrl: 'https://api.example.com'));
getIt.registerSingleton<AuthService>(AuthService());
getIt.registerSingleton<DatabaseService>(DatabaseService());
},
child: child,
);
}However, mock services are recommended when you want:
- Isolated testing of specific UI states
- Fast rendering without network/database delays
- Controlled scenarios (error states, edge cases, empty data)
// Mock services - great for testing specific scenarios
getIt.registerSingleton<ApiClient>(MockApiClient()); // Instant responsesChoose based on your preview goals: real services for integration-style previews, mocks for isolated UI state testing.
Async Initialization
Async initialization works normally in previews. The async factory functions are called only once, just like in your regular app. The key is to use allReady() or isReady<T>() in your widgets to wait for initialization:
Widget asyncPreviewWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
// Async registrations work perfectly - factory called once
getIt.registerSingletonAsync<ApiService>(
() async => ApiService().initialize(),
);
getIt.registerSingletonAsync<DatabaseService>(
() async => DatabaseService().connect(),
);
},
child: child,
);
}
// In your widget, wait for services to be ready
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: getIt.allReady(), // Wait for all async singletons
builder: (context, snapshot) {
if (snapshot.hasData) {
// Services are ready, use them
final api = getIt<ApiService>();
return Text(api.data);
}
return CircularProgressIndicator(); // Show loading
},
);
}
}Note: The preview environment is web-based, so file I/O (dart:io) and native plugins won't work, but network calls and most async operations work fine.
Create Reusable Wrappers
If you have common setups, create named wrappers:
// Define once
Widget basicAppWrapper(Widget child) => GetItPreviewWrapper(
init: (getIt) {
getIt.registerSingleton<ApiClient>(MockApiClient());
getIt.registerSingleton<AuthService>(MockAuthService());
},
child: child,
);
// Reuse everywhere
@Preview(name: 'Widget 1', wrapper: basicAppWrapper)
Widget widget1Preview() => const Widget1();
@Preview(name: 'Widget 2', wrapper: basicAppWrapper)
Widget widget2Preview() => const Widget2();Combine with Other Preview Parameters
You can use get_it wrappers alongside other preview features:
@Preview(
name: 'Responsive Dashboard',
wrapper: myPreviewWrapper,
size: Size(375, 812), // iPhone 11 Pro size
textScaleFactor: 1.3, // Accessibility testing
)
Widget dashboardPreview() => const DashboardWidget();Troubleshooting
"get_it: Object/factory with type X is not registered"
Your preview function is being called before get_it is initialized. Use one of the two approaches above to register services before accessing them.
Preview not updating on hot reload
The wrapper's dispose() might not be called. Try stopping and restarting the preview, or use the direct registration approach with isRegistered() checks.
Services persisting between previews
If using direct registration without cleanup, services remain in get_it. Either:
- Use the wrapper approach (automatic
reset()on dispose) - Manually call
await GetIt.I.reset()when needed - Use separate named instances for different previews
Learn More
- Flutter Widget Previewer Documentation
get_itTesting Guide - Similar patterns for unit testsget_itScopes - For more advanced isolation needs
Next Steps
- Try both approaches in your project
- Create reusable wrapper functions for common scenarios
- Explore combining previews with different themes and sizes
- Check out the complete example in the get_it repository