Flutter Widget Previews
Esta guía te muestra cómo usar get_it con el widget previewer de Flutter.
El Desafío
Cuando usas la anotación @Preview, Flutter renderiza tu widget sin llamar a main() o ejecutar el código de inicio de tu app. Esto significa:
get_itno ha sido inicializado- No se han registrado servicios
- Los widgets que llaman a
GetIt.I<SomeService>()lanzarán errores
Necesitas manejar la inicialización de get_it dentro del preview mismo.
Dos Enfoques
Hay dos formas de inicializar get_it para previews, cada una con diferentes compromisos:
- Registro Directo - Patrón simple de verificar y registrar
- Widget Wrapper - Wrapper reutilizable con limpieza automática
Elige según tus necesidades:
| Enfoque | Mejor Para | Pros | Contras |
|---|---|---|---|
| Registro Directo | Previews simples, de una sola vez | Control máximo, código mínimo | Sin limpieza automática, guards manuales |
| Widget Wrapper | Configuraciones reutilizables, múltiples previews | Limpieza automática, principio DRY | Configuración ligeramente mayor |
Enfoque 1: Registro Directo
El enfoque más simple es verificar si los servicios están registrados y registrarlos si no lo están.
Cómo Funciona
Flutter puede llamar a tu función de preview múltiples veces (en hot reload, etc.), así que proteges contra registro doble usando 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(),
),
);
}Cuándo Usar
- Previews de una sola vez con dependencias únicas
- Prototipado rápido donde quieres resultados inmediatos
- Control máximo sobre el timing de inicialización
Pros y Contras
Pros:
- Código mínimo, fácil de entender
- Control completo sobre el orden de registro
- No se necesitan widgets adicionales
Contras:
- Verificaciones de guard manuales para cada servicio
- Sin limpieza automática (permanece en
get_ithasta reset manual) - Duplicación de código si múltiples previews necesitan la misma configuración
Enfoque 2: Widget Wrapper
Para mejor organización y reutilización, crea un widget wrapper que maneje inicialización y limpieza automáticamente.
El Widget Wrapper
Primero, crea un widget wrapper reutilizable:
/// 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;
}
}Usando el Wrapper
Usa el wrapper con el parámetro wrapper de la anotación @Preview:
// 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(),
),
);Cuándo Usar
- Múltiples previews que comparten las mismas dependencias
- Configuraciones reutilizables a través de diferentes previews de widgets
- Limpieza automática vía
reset()en dispose - Código más limpio con separación de responsabilidades
Pros y Contras
Pros:
- Limpieza automática cuando el preview es dispuesto
- Reutilizable a través de múltiples previews
- Código de preview más limpio (la configuración está separada)
- Fácil de crear múltiples configuraciones
Contras:
- Requiere definición de widget wrapper separada
- La función wrapper debe ser de nivel superior o estática
- Configuración inicial ligeramente mayor
Probando Diferentes Escenarios
Un uso poderoso del enfoque wrapper es crear diferentes escenarios para el mismo 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();Este patrón es excelente para:
- Probar casos extremos (estados de error, datos vacíos, carga)
- Diferentes estados de usuario (loggeado, desloggeado, invitado)
- Pruebas de accesibilidad (diferentes tamaños de fuente, temas)
- Diseño responsivo (diferentes tamaños de pantalla con parámetro
size)
Ejemplo Completo
Mira la app de ejemplo de get_it para un ejemplo completo funcionando mostrando ambos enfoques.
El ejemplo incluye:
preview()- Enfoque de registro directopreviewWithWrapper()- Enfoque wrapper- Implementación de
GetItPreviewWrapperen preview_wrapper.dart
Consejos y Mejores Prácticas
Usando Servicios Reales vs Mock
Uno de los beneficios clave de get_it es que puedes conectar tus widgets a servicios reales en previews, permitiéndote ver tus widgets con datos y comportamiento reales. Solo necesitas asegurar inicialización apropiada:
// Servicios reales - perfectamente válido si se inicializa apropiadamente
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,
);
}Sin embargo, se recomiendan servicios mock cuando quieres:
- Pruebas aisladas de estados específicos de UI
- Renderizado rápido sin retrasos de red/base de datos
- Escenarios controlados (estados de error, casos extremos, datos vacíos)
// Servicios mock - excelente para probar escenarios específicos
getIt.registerSingleton<ApiClient>(MockApiClient()); // Respuestas instantáneasElige según los objetivos de tu preview: servicios reales para previews estilo integración, mocks para pruebas aisladas de estado de UI.
Inicialización Async
La inicialización async funciona normalmente en previews. Las funciones factory async se llaman solo una vez, igual que en tu app regular. La clave es usar allReady() o isReady<T>() en tus widgets para esperar la inicialización:
Widget asyncPreviewWrapper(Widget child) {
return GetItPreviewWrapper(
init: (getIt) {
// Los registros async funcionan perfectamente - factory llamada una vez
getIt.registerSingletonAsync<ApiService>(
() async => ApiService().initialize(),
);
getIt.registerSingletonAsync<DatabaseService>(
() async => DatabaseService().connect(),
);
},
child: child,
);
}
// En tu widget, espera a que los servicios estén listos
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: getIt.allReady(), // Espera a que todos los async singletons estén listos
builder: (context, snapshot) {
if (snapshot.hasData) {
// Servicios están listos, úsalos
final api = getIt<ApiService>();
return Text(api.data);
}
return CircularProgressIndicator(); // Muestra carga
},
);
}
}Nota: El entorno de preview está basado en web, así que I/O de archivos (dart:io) y plugins nativos no funcionarán, pero llamadas de red y la mayoría de operaciones async funcionan bien.
Crea Wrappers Reutilizables
Si tienes configuraciones comunes, crea wrappers con nombre:
// Define una vez
Widget basicAppWrapper(Widget child) => GetItPreviewWrapper(
init: (getIt) {
getIt.registerSingleton<ApiClient>(MockApiClient());
getIt.registerSingleton<AuthService>(MockAuthService());
},
child: child,
);
// Reutiliza en todas partes
@Preview(name: 'Widget 1', wrapper: basicAppWrapper)
Widget widget1Preview() => const Widget1();
@Preview(name: 'Widget 2', wrapper: basicAppWrapper)
Widget widget2Preview() => const Widget2();Combina con Otros Parámetros de Preview
Puedes usar wrappers de get_it junto con otras características de preview:
@Preview(
name: 'Responsive Dashboard',
wrapper: myPreviewWrapper,
size: Size(375, 812), // Tamaño de iPhone 11 Pro
textScaleFactor: 1.3, // Prueba de accesibilidad
)
Widget dashboardPreview() => const DashboardWidget();Solución de Problemas
"get_it: Object/factory with type X is not registered"
Tu función de preview se está llamando antes de que get_it sea inicializado. Usa uno de los dos enfoques anteriores para registrar servicios antes de accederlos.
Preview no se actualiza en hot reload
El dispose() del wrapper podría no estar siendo llamado. Intenta detener y reiniciar el preview, o usa el enfoque de registro directo con verificaciones isRegistered().
Servicios persistiendo entre previews
Si usas registro directo sin limpieza, los servicios permanecen en get_it. Ya sea:
- Usa el enfoque wrapper (automático
reset()en dispose) - Llama manualmente a
await GetIt.I.reset()cuando sea necesario - Usa instancias con nombre separadas para diferentes previews
Aprende Más
- Documentación del Widget Previewer de Flutter
- Guía de Pruebas de
get_it- Patrones similares para pruebas unitarias - Scopes de
get_it- Para necesidades de aislamiento más avanzadas
Próximos Pasos
- Prueba ambos enfoques en tu proyecto
- Crea funciones wrapper reutilizables para escenarios comunes
- Explora combinar previews con diferentes temas y tamaños
- Revisa el ejemplo completo en el repositorio de get_it