DEV Community

Cover image for Dependency Injection in Flutter: Simplifying App Architecture
Bestaoui Aymen
Bestaoui Aymen

Posted on

Dependency Injection in Flutter: Simplifying App Architecture

What is Dependency Injection?

Dependency Injection is a technique where an object receives its dependencies from an external source rather than creating them itself. This promotes loose coupling and enhances code flexibility.

Types of Dependency Injection

  • Constructor Injection: Passing dependencies via a constructor.
  • Setter Injection: Providing dependencies through setter methods.
  • Method Injection: Supplying dependencies as method parameters.

In Flutter, constructor injection is the most common approach due to its simplicity and alignment with Dart’s object-oriented nature.

Why Use Dependency Injection in Flutter?

  • Modularity: Swap implementations (e.g., mock services for testing) without changing the core logic.
  • Testability: Easily mock dependencies for unit tests.
  • Scalability: Manage complex dependency trees in large apps.
  • Maintainability: Reduce tight coupling between classes.

Popular DI Solutions in Flutter

Several packages simplify DI in Flutter:

  • get_it: A lightweight service locator, easy to set up and widely used.
  • provider: Often used for state management but can handle DI.
  • injectable: A code-generator-based DI solution for advanced setups.
  • kiwi: Another service locator with a focus on simplicity.

This blog focuses on get_it due to its popularity and ease of use.

Setting Up get_it for Dependency Injection

Let’s walk through implementing DI in a Flutter app using get_it. We’ll build a simple app that fetches and displays user data from a mock API.

Step 1: Add Dependencies

Add get_it to your pubspec.yaml:

dependencies:
  get_it: ^8.2.0
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install the package.

Step 2: Create a Service Locator

Set up a singleton instance of GetIt to register and access dependencies.

import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

void setupDependencies() {
  getIt.registerSingleton<UserService>(UserService());
  getIt.registerFactory<UserRepository>(() => UserRepository(getIt<UserService>()));
}
Enter fullscreen mode Exit fullscreen mode
  • registerSingleton: Creates a single instance reused throughout the app.
  • registerFactory: Creates a new instance each time the dependency is requested.
  • registerLazySingleton: Creates a single instance only when first requested.

Step 3: Define Services and Repositories

Create a simple service and repository to demonstrate DI.

// user_service.dart
class UserService {
  Future<String> fetchUserName() async {
    // Simulate API call
    await Future.delayed(const Duration(seconds: 1));
    return 'John Doe';
  }
}

// user_repository.dart
class UserRepository {
  final UserService userService;

  UserRepository(this.userService);

  Future<String> getUserName() async {
    return userService.fetchUserName();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Integrate DI in Your Flutter App

Use the registered dependencies in a Flutter widget.

import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

void main() {
  setupDependencies(); // Initialize dependencies
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const UserScreen(),
    );
  }
}

class UserScreen extends StatefulWidget {
  const UserScreen({super.key});

  @override
  _UserScreenState createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
  String _userName = 'Loading...';

  @override
  void initState() {
    super.initState();
    _loadUserName();
  }

  Future<void> _loadUserName() async {
    final repository = getIt<UserRepository>();
    final name = await repository.getUserName();
    setState(() {
      _userName = name;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User Profile')),
      body: Center(
        child: Semantics(
          label: 'User name display',
          child: Text(
            'User: $_userName',
            style: const TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • Service Locator: getIt holds the UserService and UserRepository instances.
  • Dependency Injection: UserRepository receives UserService via constructor injection.
  • Widget Integration: The UserScreen widget accesses UserRepository using getIt<UserRepository>() to fetch data.

Best Practices for Dependency Injection

  1. Centralize Registration: Define all dependencies in a single setupDependencies function for clarity.
  2. Use Singleton for Services: Register services like APIs or databases as singletons to avoid redundant instances.
  3. Test with Mocks: Replace real services with mocks during testing using getIt.reset() or getIt.registerSingleton<MockService>.
  4. Keep It Simple: Avoid overcomplicating DI setups for small apps; use get_it or provider for straightforward needs.
  5. Combine with State Management: Integrate DI with state management solutions like provider or Riverpod for cohesive architecture.

Common Pitfalls

  • Overusing Singletons: Avoid registering everything as a singleton; use factories for objects that need fresh instances.
  • Circular Dependencies: Ensure dependencies don’t create cycles, causing runtime errors.

Top comments (0)