Improve Your Dart Code with Dependency Injection: Understanding Loose and Tight Coupling

Sepehr
5 min readJun 26, 2024

--

Source: https://www.dhiwise.com/

In software design, “loose coupling” and “tight coupling” are important terms. They help us understand how connected different parts of the code are. This article explains these terms and shows why Dependency Injection (DI) is a good practice in Dart. We will use simple examples to make it clear.

What is Coupling?

Coupling refers to how much one part of the code knows about another.

Tight Coupling

Tight coupling happens when one part of the code is very dependent on another. This makes it hard to change or test the code.

Example of Tight Coupling: Initialization in the Constructor

class ApiService {
Future<String> fetchData() async {
// Simulate a network call
return "Real data from API";
}
}

class DataRepository {
final ApiService apiService;

DataRepository() : apiService = ApiService(); // Initialization in the constructor

Future<String> getData() {
return apiService.fetchData();
}
}

In this example, DataRepository creates an ApiService directly. This makes it hard to change ApiService later.

Example of Tight Coupling: Initialization in the Class

class ApiService {
Future<String> fetchData() async {
// Simulate a network call
return "Real data from API";
}
}

class DataRepository {
final ApiService apiService = ApiService(); // Initialization in the class

Future<String> getData() {
return apiService.fetchData();
}
}

Here, DataRepository also creates ApiService directly, causing the same problem.

What is Dependency Injection (DI)?

Dependency Injection (DI) is a way to provide parts of the code with what they need from outside. This means a class does not create its dependencies. Instead, it receives them from another place.

Why Use DI in Dart?

  1. Better Testing

DI makes it easy to replace real services with mock ones in tests. This makes tests run faster and more reliably.

Example of Testing with DI:

void main() {
final mockApiService = MockApiService();
final dataRepository = DataRepository(mockApiService);

dataRepository.getData().then((data) {
print(data); // Should print "Mock data"
});
}

Using a mock ApiService, you can test DataRepository without real network calls.

2. Loose Coupling

Loose coupling occurs when classes are minimally dependent on each other. This is achieved by reducing the direct knowledge one class has about another, often by using interfaces or abstract classes. Loose coupling enhances flexibility and makes the code easier to test and maintain.

Example of Loose Coupling using Dependency Injection:

abstract class ApiService {
Future<String> fetchData();
}

class ApiServiceImp implements ApiService {
@override
Future<String> fetchData() async {
// Simulate a network call
return "Real data from API";
}
}

class DataRepository {
final ApiService apiService;

DataRepository(this.apiService); // Dependency Injection

Future<String> getData() {
return apiService.fetchData();
}
}

// Mock ApiService for testing
class MockApiService implements ApiService {
@override
Future<String> fetchData() async {
return "Mock data";
}
}

Here, DataRepository depends on an abstract ApiService, and the actual implementation is injected via the constructor. This approach promotes loose coupling, making DataRepository easier to test and modify. Easier Maintenance

DI makes the code easier to maintain. If a dependency changes, you can update it without changing the class that uses it.

Example of Enhanced Maintainability:

class LoggingApiService implements ApiService {
final ApiService _apiService;

LoggingApiService(this._apiService);

@override
Future<String> fetchData() async {
print("Fetching data...");
final data = await _apiService.fetchData();
print("Data fetched: $data");
return data;
}
}

You can add new features, like logging, without changing DataRepository.

Unit testing is a software testing technique that involves testing individual units or components of a software application in isolation to ensure they function correctly.

Unit testing is a software testing technique that involves testing individual units or components of a software application in isolation to ensure they function correctly.

what do you think is the difference between using dependency injection in Unit testing and not using it in Unit testing?

Let’s look at the result of an example together.

Without Dependency Injection

class ApiService {
Future<String> fetchData() async {
// Simulate a network call
return "Real data from API";
}
}

class DataRepository {
final ApiService _apiService = ApiService();

Future<String> getData() {
return _apiService.fetchData();
}
}

// Unit test with real network dependency
void main() {
final dataRepository = DataRepository();

dataRepository.getData().then((data) {
print(data); // Will print "Real data from API" or fail if no internet
});
}

Using Dependency Injection

abstract class ApiService {
Future<String> fetchData();
}

class ApiServiceImp implements ApiService {
@override
Future<String> fetchData() async {
// Simulate a network call
return "Real data from API";
}
}

class DataRepository {
final ApiService apiService;

DataRepository(this.apiService);

Future<String> getData() {
return apiService.fetchData();
}
}

// Mock ApiService for testing
class MockApiService implements ApiService {
@override
Future<String> fetchData() async {
return "Mock data";
}
}

// Unit test using the mock implementation
void main() {
final mockApiService = MockApiService();
final dataRepository = DataRepository(mockApiService);

dataRepository.getData().then((data) {
print(data); // Should print "Mock data"
});
}
  • With DI, you don’t need real network responses during tests. Mocks provide controlled and immediate responses.
  • Without DI, your tests wait for real network responses, which can cause failures if the network is unavailable or slow.

By using DI, you ensure your unit tests are more robust and isolated from external dependencies, such as the network.

Conclusion

Using Dependency Injection in Dart makes your code easier to test, change, and maintain. By keeping parts of your code separate, you make it more flexible and robust. Whether you are working on a small project or a large application, DI is a valuable technique that helps you write better code.

By adopting DI, you ensure that your Dart applications are well-structured and ready for future changes, leading to a smoother development process and a higher-quality product.

Join me in the next section…

source: https://pin.it/2BEgger5U

next section I want to write an article about Dependency injection and Service locator in Dart.

I hope you enjoyed the article. Thanks for reading! 🥳️

GET IN TOUCH:

GitHub: SepehrTabeian

Linkedin: SepehrTabeian

I invite you to join the Persian Flutter Community to enhance your knowledge and skills.
Youtube: PersianFlutter

--

--

Sepehr

Proficient in Java, Kotlin, Dart, Swift, and cross-platform development, I excel in creating user-centric applications that marry functionality with design