Principles
- DI is registered in a single entry
lib/core/di/di_setup.dart의diSetup()function. Do not split it up per feature (since this is a single-package structure). main()inrunApp()calling beforediSetup()must be called.- Only in the Root widget, retrieve the ViewModel with
getIt<T>(). Never usegetItin Screen widgets or shared widgets (to ensure testability). - ViewModel은 Always
registerFactoryregistered with. If it is a singleton, multiple screens end up sharing the same state.
Default entry point
// lib/core/di/di_setup.dart
final getIt = GetIt.instance;
void diSetup() { // 1) data source // 2) repository // 3) use case // 4) view model }
```dart
// lib/main.dart
void main() {
diSetup();
runApp(const MyApp());
}
The registration order goes from the leaves of the dependency graph upward. It must be in the order DataSource → Repository → UseCase → ViewModel so that when getIt() references it, it is already registered.
Registration pattern per layer
DataSource (singleton)
getIt.registerSingleton<RecipeDataSource>(RemoteRecipeDataSourceImpl());
getIt.registerSingleton<LocalStorage>(DefaultLocalStorage());
Register by interface type so that the implementation is never referenced directly from the Root/ViewModel.
Repository (singleton)
Constructor parameters are getIt() passed by unpacking with. If generic inference is needed, explicitly write getIt<Foo>() .
getIt.registerSingleton<RecipeRepository>(
MockRecipeRepositoryImpl(
recipeDataSource: getIt(),
),
);
getIt.registerSingleton<BookmarkRepository>(MockBookmarkRepositoryImpl());
UseCase (singleton)
Since it is a pure function with no state, a singleton is sufficient. Type parameters can be omitted (since there is only one implementation class, Dart can infer it).
getIt.registerSingleton(
GetSavedRecipesUseCase(
recipeRepository: getIt(),
bookmarkRepository: getIt(),
),
);
ViewModel (factory)
Always registerFactory use. This is because a new instance is needed when reopening the screen.
getIt.registerFactory<IngredientViewModel>(
() => IngredientViewModel(
ingredientRepository: getIt(),
procedureRepository: getIt(),
getDishesByCategoryUseCase: getIt(),
clipboardService: getIt(),
),
);
Injecting in the Root widget
class SavedRecipesRoot extends StatelessWidget {
const SavedRecipesRoot({super.key});
@override
Widget build(BuildContext context) {
final viewModel = getIt<SavedRecipesViewModel>();
return ListenableBuilder(
listenable: viewModel,
builder: (context, _) => SavedRecipesScreen(
recipes: viewModel.state.recipes,
onAction: viewModel.onAction,
),
);
}
}
Principles: getIt<>() Calls are *Root in the widget's build() only within. Keep the Screen widget receiving only state 와 onAction so that previews/tests are possible.
Singleton vs factory
| Registration function | When to use | 예 |
|---|---|---|
registerSingleton<T>(instance) |
When only one instance is needed for the app's lifetime. When putting in an already-created object directly | Repository, DataSource, UseCase, HttpClient |
registerLazySingleton<T>(() => ...) |
When you want to delay creation until the first access | Objects with high initialization cost |
registerFactory<T>(() => ...) |
New instance on every call | ViewModel |
Actual examples in this project (lib/core/di/di_setup.dart) have Repository/DataSource/UseCase consistently separated with registerSingleton, ViewModel은 registerFactory . Follow this rule.
When a factory method is needed
If it does not end with a single constructor and you need to call a factory method, use the lambda form.
getIt.registerSingleton<HttpClient>(HttpClientFactory.create(getIt()));
The same lambda form is used when named parameters are needed.
Checklist — DI registration when adding a new feature
- The DataSource interface is
domainordata/data_source/defined in - the implementation in
registerSingleton<Interface>Register as (specify interface type) - Register Repository interface/implementation
- Register UseCase (singleton if stateless)
- ViewModel은 Always
registerFactory - only in the Root widget
getIt<ViewModelType>()retrieve with
Anti-pattern
- ❌
getIt<T>()call inside Screen widgets, shared widgets, or deep in the StatelessWidget tree → testing and previews become impossible. - ❌ ViewModel을
registerSingletonregister as → state becomes corrupted when re-entering the screen. - ❌
diSetup()outsidegetIt.register*secretly call → registration order and dependency graph become scattered. - ❌ with the implementation class type
getItretrieve from (getIt<MockRecipeRepositoryImpl>()) → retrieve by interface type so the implementation can be swapped.