get_it 및 injectable을 활용한 의존성 주입
get_it 서비스 로케이터와 injectable 코드 생성을 사용해 의존성을 등록하고 해결합니다.
get_it 및 injectable을 활용한 의존성 주입은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why a Service Locator?
In a clean, modular Flutter app you want your presentation layer to depend on abstractions, not on the concrete classes that build them. Manually constructing objects (RemoteApi(HttpClient(...))) everywhere leaks construction details and makes testing painful.
A service locator centralizes object creation and lookup. get_it is the de-facto service locator for Dart/Flutter: you register how to build a type once, then resolve it anywhere with a single call.
- Decoupling — widgets ask for an interface, not a constructor.
- Testability — swap a real implementation for a fake in tests.
- Lifecycle control — singleton vs. fresh instance per request.
The GetIt Instance
GetIt exposes a global singleton via GetIt.instance (commonly aliased getIt or sl). You can also create isolated instances with GetIt.asNewInstance() for tests.
The plain Dart API mirrors what code generation will produce later, so it pays to understand it first. Below is the locator pattern modeled with plain classes — no Flutter needed.
// A tiny hand-rolled service locator to show the idea.
class Locator {
final _factories = <Type, Object Function()>{};
final _singletons = <Type, Object>{};
void registerFactory<T>(T Function() create) =>
_factories[T] = () => create() as Object;
void registerSingleton<T>(T instance) =>
_singletons[T] = instance as Object;
T get<T>() {
if (_singletons.containsKey(T)) return _singletons[T] as T;
final f = _factories[T];
if (f == null) throw StateError('No registration for $T');
return f() as T;
}
}
class ApiClient {
final String baseUrl;
ApiClient(this.baseUrl);
}
void main() {
final sl = Locator();
sl.registerSingleton<ApiClient>(ApiClient('https://api.example.com'));
final api = sl.get<ApiClient>();
print('Resolved ApiClient -> ${api.baseUrl}');
}Registration Lifetimes
get_it offers three core registration kinds. Choosing correctly is the key architectural decision:
registerFactory<T>()— runs the builder every time you resolve. Use for short-lived, stateful objects (e.g. a fresh BLoC per screen).registerSingleton<T>(instance)— you provide a ready instance; created eagerly at startup.registerLazySingleton<T>()— built once on first resolve, then cached. Ideal for repositories and API clients you don't need until used.
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void configureDependencies() {
getIt.registerLazySingleton<Dio>(() => Dio());
getIt.registerLazySingleton<AuthRemoteSource>(
() => AuthRemoteSource(getIt<Dio>()),
);
getIt.registerFactory<LoginBloc>(
() => LoginBloc(getIt<AuthRemoteSource>()),
);
}Registering Against Abstractions
Clean Architecture says the domain layer defines an interface (e.g. AuthRepository) and the data layer implements it (AuthRepositoryImpl). Register the abstract type as the generic, and return the concrete implementation from the builder.
Consumers resolve getIt<AuthRepository>() and never learn the implementation — you can swap it without touching call sites.
abstract class AuthRepository {
Future<String> login(String email, String password);
}
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteSource remote;
AuthRepositoryImpl(this.remote);
@override
Future<String> login(String email, String password) =>
remote.authenticate(email, password);
}
void registerRepositories() {
// Generic is the ABSTRACTION, builder returns the IMPL.
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(getIt<AuthRemoteSource>()),
);
}Resolving Inside Widgets
From any widget you call the locator directly. Because BLoCs are usually registerFactory, each screen gets a fresh instance, and you dispose it with the widget.
A common pattern: resolve the factory-built BLoC in BlocProvider(create: ...), while repositories/clients stay lazy singletons shared across the app.
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<LoginBloc>(
// Fresh BLoC from the factory registration.
create: (_) => getIt<LoginBloc>(),
child: const _LoginView(),
);
}
}Enter injectable
Writing configureDependencies() by hand grows unwieldy in a large modular app. injectable is a code generator that scans annotations and emits the registration code for you, wiring constructor parameters automatically.
You need three dev/runtime dependencies:
injectable— the annotations.get_it— the runtime locator it targets.injectable_generator+build_runner— dev-only code generation.
# pubspec.yaml
dependencies:
get_it: ^7.7.0
injectable: ^2.4.0
dev_dependencies:
build_runner: ^2.4.0
injectable_generator: ^2.6.0The @injectable Annotation
Annotate a class with @injectable and the generator registers it as a factory. Its constructor parameters are resolved from the locator recursively, so you never wire them by hand.
To bind an interface to an implementation, annotate the impl and use @Injectable(as: AuthRepository) — the generic registration becomes the abstraction.
import 'package:injectable/injectable.dart';
@injectable
class LoginBloc {
final AuthRepository repository;
LoginBloc(this.repository); // injected automatically
}
@Injectable(as: AuthRepository)
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteSource remote;
AuthRepositoryImpl(this.remote);
// ...
}Singletons and Lazy Singletons
injectable mirrors get_it's lifetimes through annotations:
@singleton— eager singleton, created when DI is configured.@lazySingleton— created on first resolve, then cached.@injectable— factory (new instance each time).
Pick @lazySingleton for repositories, data sources, and clients; @injectable for BLoCs/Cubits scoped to a screen.
@lazySingleton
class AuthRemoteSource {
final Dio dio;
AuthRemoteSource(this.dio);
}
@singleton
class AppConfig {
final String environment;
AppConfig() : environment = const String.fromEnvironment('ENV');
}Third-Party Types with @module
You can't annotate classes you don't own (Dio, SharedPreferences). A register module solves this: declare an abstract class annotated @module, and expose getters/methods that build those types.
Async dependencies (like SharedPreferences.getInstance()) return a Future and are registered as @preResolve so DI awaits them at startup.
import 'package:injectable/injectable.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
@module
abstract class RegisterModule {
@lazySingleton
Dio get dio => Dio(BaseOptions(baseUrl: 'https://api.example.com'));
@preResolve
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
}Generating and Wiring configureDependencies
Create a single entry point annotated with @InjectableInit. Run the generator and it emits *.config.dart containing init(getIt), which you call from your function.
Generate with:
dart run build_runner build --delete-conflicting-outputs
Then call configureDependencies() before runApp. With @preResolve dependencies present, the function is async and must be awaited.
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart'; // generated
final getIt = GetIt.instance;
@InjectableInit(
initializerName: 'init',
preferRelativeImports: true,
asExtension: true,
)
Future<void> configureDependencies() => getIt.init();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies();
runApp(const MyApp());
}Environments and Test Overrides
For testing you replace real registrations with fakes. With plain get_it you call getIt.unregister<T>() then re-register a mock, or use a fresh GetIt.asNewInstance(). With injectable, the @Environment annotation (e.g. @dev, @test) selects implementations per environment passed to init.
The pattern below shows the test-override idea with plain Dart so it runs standalone.
abstract class Clock {
DateTime now();
}
class SystemClock implements Clock {
@override
DateTime now() => DateTime.now();
}
class FixedClock implements Clock {
final DateTime fixed;
FixedClock(this.fixed);
@override
DateTime now() => fixed;
}
void main() {
final registry = <Type, Object>{};
void register<T>(T impl) => registry[T] = impl as Object;
T resolve<T>() => registry[T] as T;
register<Clock>(SystemClock());
// Override for a deterministic test:
register<Clock>(FixedClock(DateTime.utc(2030, 1, 1)));
print('Test clock now: ${resolve<Clock>().now()}');
}Quick Check
You have a UserRepository backed by a Dio HTTP client shared across the app, and a ProfileCubit that holds per-screen UI state and must be disposed when its screen closes. Which injectable annotations best fit each?
Recap
You now know how to wire dependencies in a modular Flutter app:
- get_it is the runtime service locator —
registerFactory,registerSingleton, andregisterLazySingletoncontrol lifetime. - Register against abstractions (
registerLazySingleton<AuthRepository>) so call sites stay decoupled from implementations. - injectable generates that registration code from annotations:
@injectable(factory),@lazySingleton,@singleton, and@Injectable(as: ...)to bind interfaces. - Use
@modulewith@preResolvefor third-party and async dependencies likeDioandSharedPreferences. @InjectableInit+build_runnerproducegetIt.init(); awaitconfigureDependencies()beforerunApp.
Rule of thumb: lazy singletons for repositories/clients, factories for screen-scoped BLoCs/Cubits.
자주 묻는 질문
“get_it 및 injectable을 활용한 의존성 주입” 강의는 무료인가요?
네 — “get_it 및 injectable을 활용한 의존성 주입” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.
“get_it 및 injectable을 활용한 의존성 주입”에서 뭘 배우나요?
get_it 서비스 로케이터와 injectable 코드 생성을 사용해 의존성을 등록하고 해결합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“get_it 및 injectable을 활용한 의존성 주입” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 도메인, 데이터 및 프레젠테이션 계층의 경계
- get_it 및 injectable을 활용한 의존성 주입
- 기능 우선 폴더 구조 및 Melos 모노레포
- Either, 실패 유형 및 함수형 오류 처리