0Pricing
Flutter Mobile Development · レッスン

get_itとinjectableによる依存性注入

get_itのサービスロケーターとinjectableのコード生成を使い、依存関係を登録して解決します。

「get_itとinjectableによる依存性注入」はCoddyKit上の無料Flutter Mobile Developmentレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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.0

The @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, and registerLazySingleton control 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 @module with @preResolve for third-party and async dependencies like Dio and SharedPreferences.
  • @InjectableInit + build_runner produce getIt.init(); await configureDependencies() before runApp.

Rule of thumb: lazy singletons for repositories/clients, factories for screen-scoped BLoCs/Cubits.

よくある質問

「get_itとinjectableによる依存性注入」レッスンは無料ですか?

はい。「get_itとinjectableによる依存性注入」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Flutter Mobile Developmentコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Flutter Mobile Developmentコースには全4レッスンが含まれています。

「get_itとinjectableによる依存性注入」で何を学びますか?

get_itのサービスロケーターとinjectableのコード生成を使い、依存関係を登録して解決します。 ブラウザで直接実行するハンズオンコードでFlutter Mobile Developmentを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Flutter Mobile Developmentを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのFlutter Mobile Developmentは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「get_itとinjectableによる依存性注入」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このFlutter Mobile Developmentレッスンでコードを書いて実行できますか?

はい。すべてのFlutter Mobile Developmentレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ドメイン、データ、プレゼンテーション層の境界
  2. get_itとinjectableによる依存性注入
  3. フィーチャーファーストのフォルダー構成とMelosモノレポ
  4. Either、失敗型、関数型エラーハンドリング
← Flutter Mobile Developmentに戻る