Lesson 40 of 51 intermediate

GetIt Service Locator

The universal toolbox for your app's dependencies

Open interactive version (quiz + challenge)

Real-world analogy

Imagine a hospital. Doctors do not build their own stethoscopes or mix their own medicines. There is a central supply room (GetIt) where everything is registered and organized. When a doctor needs a syringe, they go to the supply room and ask for one. The supply room either gives them a fresh one (factory) or the same shared one that all doctors use (singleton). GetIt is your app's supply room -- it holds all the services and hands them out when needed.

What is it?

GetIt is a service locator package for Dart that acts as a central registry for all your app's dependencies. You register services (like Dio, repositories, use cases, and BLoCs) at app startup, and then retrieve them anywhere in your code without needing BuildContext. It supports singletons (one shared instance), lazy singletons (created on first use), and factories (new instance each time). In team_mvp_kit, the DependencyManager class organizes all registrations by layer: network, data sources, repositories, use cases, and BLoCs.

Real-world relevance

In team_mvp_kit, GetIt is the backbone that wires everything together. The DependencyManager is called in main() before runApp. It registers Dio as a singleton (one HTTP client for the whole app), repositories as lazy singletons (created when first needed), use cases as factories (stateless, so new instances are fine), and BLoCs as factories (each screen gets its own BLoC instance). During testing, the team resets GetIt and registers mocks, making it trivial to test any layer in isolation. Without GetIt, you would need to pass dependencies through constructors all the way from main() down to every widget.

Key points

Code example

import 'package:get_it/get_it.dart';
import 'package:dio/dio.dart';

final getIt = GetIt.instance;

// ---- DependencyManager (team_mvp_kit pattern) ----

class DependencyManager {
  static Future<void> init() async {
    _registerNetwork();
    _registerDataSources();
    _registerRepositories();
    _registerUseCases();
    _registerBlocs();
  }

  static void _registerNetwork() {
    getIt.registerLazySingleton<Dio>(() {
      final dio = Dio(BaseOptions(
        baseUrl: 'https://api.example.com',
        connectTimeout: const Duration(seconds: 10),
        receiveTimeout: const Duration(seconds: 10),
        headers: {'Content-Type': 'application/json'},
      ));
      dio.interceptors.add(
        LogInterceptor(
          requestBody: true,
          responseBody: true,
        ),
      );
      return dio;
    });
  }

  static void _registerDataSources() {
    getIt.registerLazySingleton<TaskRemoteDataSource>(
      () => TaskRemoteDataSourceImpl(getIt<Dio>()),
    );
  }

  static void _registerRepositories() {
    getIt.registerLazySingleton<TaskRepository>(
      () => TaskRepositoryImpl(
        getIt<TaskRemoteDataSource>(),
      ),
    );
  }

  static void _registerUseCases() {
    getIt.registerFactory<GetTasksUseCase>(
      () => GetTasksUseCase(getIt<TaskRepository>()),
    );
    getIt.registerFactory<CreateTaskUseCase>(
      () => CreateTaskUseCase(getIt<TaskRepository>()),
    );
    getIt.registerFactory<UpdateTaskStatusUseCase>(
      () => UpdateTaskStatusUseCase(
          getIt<TaskRepository>()),
    );
  }

  static void _registerBlocs() {
    getIt.registerFactory<TaskListBloc>(
      () => TaskListBloc(
        getTasks: getIt<GetTasksUseCase>(),
        createTask: getIt<CreateTaskUseCase>(),
      ),
    );
  }
}

// ---- main.dart ----

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize all dependencies
  await DependencyManager.init();

  // Set up BLoC observer for debugging
  Bloc.observer = AppBlocObserver();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        // Global BLoCs provided at app level
        BlocProvider(
          create: (_) => getIt<AuthBloc>()
              ..add(CheckAuthStatus()),
        ),
      ],
      child: MaterialApp.router(
        title: 'Task Manager',
        routerConfig: appRouter,
      ),
    );
  }
}

// ---- Usage in Screens ----

class TaskListScreen extends StatelessWidget {
  const TaskListScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      // GetIt resolves the full dependency chain
      create: (_) => getIt<TaskListBloc>()
          ..add(LoadTasks()),
      child: const TaskListView(),
    );
  }
}

// ---- Testing with GetIt ----

void main() {
  late MockTaskRepository mockRepo;
  late TaskListBloc bloc;

  setUp(() {
    getIt.reset();
    mockRepo = MockTaskRepository();

    getIt.registerLazySingleton<TaskRepository>(
      () => mockRepo,
    );
    getIt.registerFactory<GetTasksUseCase>(
      () => GetTasksUseCase(getIt()),
    );
    getIt.registerFactory<CreateTaskUseCase>(
      () => CreateTaskUseCase(getIt()),
    );

    bloc = TaskListBloc(
      getTasks: getIt<GetTasksUseCase>(),
      createTask: getIt<CreateTaskUseCase>(),
    );
  });

  tearDown(() {
    bloc.close();
    getIt.reset();
  });

  // Now you can test with mocked dependencies
}

Line-by-line walkthrough

  1. 1. Create a global GetIt instance accessible throughout the app.
  2. 2. DependencyManager class organizes all registrations with a static init method.
  3. 3. _registerNetwork creates a Dio singleton with base URL, timeouts, and logging interceptor.
  4. 4. LazySingleton means Dio is only created when first requested, not at startup.
  5. 5. _registerDataSources registers the remote data source, passing Dio via getIt().
  6. 6. getIt() retrieves the previously registered Dio singleton automatically.
  7. 7. _registerRepositories registers TaskRepository implementation with its data source dependency.
  8. 8. _registerUseCases registers use cases as factories since they are stateless operations.
  9. 9. Each use case receives its repository dependency through getIt().
  10. 10. _registerBlocs registers BLoCs as factories so each screen gets a fresh instance.
  11. 11. TaskListBloc receives its use case dependencies through named parameters.
  12. 12. In main.dart, WidgetsFlutterBinding.ensureInitialized() is called before async init.
  13. 13. DependencyManager.init() sets up the entire dependency graph before the app runs.
  14. 14. BlocObserver is set up for development debugging of all BLoC events and transitions.
  15. 15. MyApp uses MultiBlocProvider for app-level BLoCs like AuthBloc.
  16. 16. getIt() retrieves a fresh AuthBloc with all dependencies resolved.
  17. 17. TaskListScreen uses BlocProvider with getIt to create a screen-level BLoC.
  18. 18. The cascade operator (..) immediately dispatches LoadTasks after creation.
  19. 19. In tests, getIt.reset() clears all registrations for a clean slate.
  20. 20. Mock implementations are registered in place of real ones for isolated testing.
  21. 21. tearDown resets GetIt to prevent test pollution between test cases.

Spot the bug

class DependencyManager {
  static void init() {
    getIt.registerLazySingleton<Dio>(
      () => Dio(),
    );
    getIt.registerLazySingleton<AuthBloc>(
      () => AuthBloc(getIt()),
    );
    getIt.registerFactory<HomeBloc>(
      () => HomeBloc(authBloc: getIt()),
    );
  }
}

// In Screen A
BlocProvider(
  create: (_) => getIt<AuthBloc>(),
  child: const LoginScreen(),
)

// In Screen B
BlocProvider(
  create: (_) => getIt<AuthBloc>(),
  child: const ProfileScreen(),
)
Need a hint?
AuthBloc is registered as a LazySingleton but used with BlocProvider in two screens. What happens when one screen disposes?
Show answer
AuthBloc is a LazySingleton, meaning there is only ONE instance. But BlocProvider automatically closes (disposes) the BLoC when the widget is removed from the tree. When Screen A is popped, BlocProvider closes the singleton AuthBloc. Now Screen B's AuthBloc reference points to a closed BLoC and will crash on any emit. Fix: For shared BLoCs, either (1) provide them at the app level with MultiBlocProvider so they outlive individual screens, or (2) use BlocProvider.value instead of BlocProvider.create for the singleton so it does not get auto-disposed, or (3) register AuthBloc as a factory if each screen should have its own instance.

Explain like I'm 5

Think of GetIt as a magical backpack that you pack at the start of a school day. Before leaving home (before runApp), you put everything you might need inside: pencils (Dio), notebooks (repositories), textbooks (use cases), and lunch (BLoCs). Some items you share with friends all day (singletons -- like one water bottle). Other items you get a fresh one whenever you need it (factories -- like tissues). During the school day, whenever any class needs supplies, you just reach into the backpack. You never have to go home to get something! And for tests, you just swap out the real textbook for a practice booklet (mock).

Fun fact

GetIt was created by Thomas Burkhart and is named after the idea of 'getting it' -- getting your dependency. It is deliberately simple compared to full-blown DI frameworks like Dagger in Android. The entire package is only about 500 lines of code, but it handles singletons, factories, lazy initialization, async setup, scopes, and disposal. It has been downloaded over 200 million times and is used in some of the largest Flutter apps in production.

Hands-on challenge

Create a complete DependencyManager for an e-commerce app with: (1) Network layer: Dio with auth interceptor, (2) Data sources: ProductRemoteDataSource, CartLocalDataSource (using Hive with async init), (3) Repositories: ProductRepository, CartRepository, AuthRepository, (4) Use cases: GetProducts, AddToCart, GetCart, Login, Logout, (5) BLoCs: ProductListBloc, CartBloc, AuthBloc. Register each with the appropriate strategy (singleton vs factory). Include async initialization for Hive and SharedPreferences. Write the main.dart that calls DependencyManager.init() and sets up the app.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart