Dependency Injection-quick overview
Mohamad Abuzaid 1 year ago
mohamad-abuzaid #general

Dependency Injection-quick overview

What are DI benefits?, What is the difference between (DI) and Service Locator? and when not to use it?

Notes“This article is not a tutorial of how to implement or use DI in your project. Its goal is just to give an overview of DI, its benefits and when to use it.”
“For the rest of the article we will take Android/Kotlin with Dagger 2 as our reference for examples and code. But, same concept goes for any other programming language”

------------------


Dependency Injection is a software design pattern in which a component or object is provided with its dependencies, rather than hard coding them within the component or object. This helps to decouple the objects from their dependencies and makes the code more maintainable, flexible and easier to test, as the dependencies can be easily swapped out or mocked.

Examples of dependency injection frameworks include:

  • Java: Spring Framework - Guice
  • Android: Dagger - Hilt - Koin
  • PHP: Symfony DependencyInjection

In Android, dependencies are often required in Activities, Fragments and other components. Without dependency injection, these dependencies would typically be created within the component’s constructor or onCreate() method.

For example, consider a simple Activity that displays a list of items from a repository:

class ItemListActivity : AppCompatActivity() {
    private lateinit var itemList: ListView
    private lateinit var itemRepository: ItemRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_item_list)

        itemList = findViewById(R.id.item_list)
        itemRepository = ItemRepositoryImpl() // ItemRepositoryImpl is a concrete implementation of ItemRepository.
    }
}

In this example, the ItemRepositoryImpl is created within the Activity’s onCreate() method. This tightly couples the Activity to the specific implementation of ItemRepository, making it difficult to test and less flexible.

To use dependency injection, we can use a library like Dagger 2 to provide the dependencies to the Activity. First, we need to define a Module that provides the dependencies:

@Module
class ItemModule {
    @Provides
    fun provideItemRepository(): ItemRepository {
        return ItemRepositoryImpl()
    }
}

This module defines a provideItemRepository() method that returns an instance of ItemRepositoryImpl.

Then, we need to create a Component that uses the module to provide the dependencies:

@Component(modules = [ItemModule::class])
interface ItemComponent {
    fun inject(activity: ItemListActivity)
}

This component defines an inject() method that can be used to provide the dependencies to the Activity.

Finally, we can use the component to inject the dependencies into the Activity:

@Component(modules = [ItemModule::class])
interface ItemComponent {
    fun inject(activity: ItemListActivity)
}

In this example, the @Inject annotation is used to indicate that the itemRepository should be provided by Dagger. The itemComponent.inject(this) line is used to instruct Dagger to provide the dependencies to the Activity.

With this setup, it’s easy to test the Activity by providing a mocked repository or by swapping out the repository implementation entirely. And it’s also easy to add more dependencies if needed and to manage all of them by modifying the ItemModule and ItemComponent class.

----------------------

Benefits of Dependency Injection


  • Separation of concerns: Dependency injection allows for a clear separation of concerns between different components in your application. For example, an Activity can focus on handling user interactions, while a Repository can focus on fetching and storing data. This makes the code more modular and easier to understand.
  • Ease of testing: With dependency injection, it’s easy to provide mocked dependencies when testing a component. For example, you can provide a mocked Repository that returns a fixed set of data, rather than hitting a real API.


// example of testing the ItemListActivity with a mocked repository
class ItemListActivityTest {
    @Test
    fun test_itemList() {
        val activity = ItemListActivity()
        val mockRepo = mock<ItemRepository>()
        `when`(mockRepo.getItems()).thenReturn(listOf(item1, item2))
        val component = DaggerItemComponent.builder()
            .itemModule(ItemModule(mockRepo))
            .build()
        component.inject(activity)
        activity.onCreate(null)
        // assert that itemList contains the correct items
    }
}
  • Flexibility: Dependency injection allows for easy swapping out of dependencies at runtime. For example, you can use different implementations of a Repository depending on whether the app is running in debug or release mode.


// example of providing different repository implementations based on build type
class ItemModule {
    @Provides
    fun provideItemRepository(context: Context): ItemRepository {
        return if (BuildConfig.DEBUG) {
            DebugItemRepository(context)
        } else {
            ProdItemRepository(context)
        }
    }
}
  • Reusability: Dependency injection allows for easy reuse of components. For example, a ViewModel can be reused across multiple Activities, and a Repository can be reused across multiple ViewModels.
  • Scalability: Dependency injection makes it easy to manage and scale large applications with many dependencies. As the application grows, the dependencies can be grouped and managed in separate modules, and the dependencies can be easily swapped out or mocked as needed.
  • Easy to understand: With the use of dagger, the object creation, and injection are handled by the dagger. So it’s easy to understand the flow of objects and how they are connected with each other.

--------------------

Dependency Injection vs Service Locator


Both dependency injection (DI) and service locator are patterns used to manage dependencies in software development, but they are used in slightly different ways and have different benefits and drawbacks.


Dependency injection (DI) is a pattern in which a component is provided with its dependencies through its constructor, method calls, or fields. The component itself does not need to know how to create or look up its dependencies; they are simply passed in.

Service locator is a pattern in which a component looks up its dependencies from a central registry, rather than having them passed in. The component itself needs to know how to look up its dependencies, and the registry needs to be configured with the correct implementations. This can make the code more difficult to test and less flexible, as the dependencies cannot be easily swapped out or mocked. Examples of popular service locator frameworks include Kodein, PicoContainer and Google Guice.

Another difference is that the service locator pattern is considered an anti-pattern. Because it adds a global state to the application, it makes it harder to reason about the dependencies in the code and testability.

It’s also worth noting that there is a gray area between those two patterns and it’s called Contextualized Dependency Lookup(CDI) which is a variation of service locator pattern and it’s considered a better practice than pure service locator pattern.

---------------------

When NOT to use DI?


Dependency injection (DI) is not always the best solution for every situation. Here are a few cases when it may not be appropriate to use DI:

  1. Simple projects: For small, simple projects with few dependencies, the overhead of configuring and using a DI framework may not be worth the benefits. In such cases, it may be simpler to just create and manage the dependencies manually.
  2. Performance-critical code: In performance-critical code, the overhead of using a DI framework may be significant. In such cases, it may be more efficient to create and manage the dependencies manually.
  3. Legacy code: If you are working with legacy code that does not use DI, it may not be feasible or practical to introduce a DI framework. In such cases, it may be better to work with the existing code and add new features without using DI.
  4. Context-specific objects: If you need to create an object that requires a context (e.g. Activity or Fragment), it is not appropriate to use DI frameworks because they cannot access the context.

These are just examples, and whether or not to use DI may depend on the specific requirements and constraints of your project. It’s important to weigh the benefits and drawbacks of using DI in each case, and to choose the best solution for the specific needs of your project.


1
440
Effective UI/UX Design in Android Apps (1/3)

Effective UI/UX Design in Android Apps (1/3)

1675112374.jpg
Mohamad Abuzaid
3 months ago
SOLID principles explained

SOLID principles explained

1675112374.jpg
Mohamad Abuzaid
1 year ago
Design Patterns - [3] Behavioral

Design Patterns - [3] Behavioral

1675112374.jpg
Mohamad Abuzaid
1 year ago
Security in Android App Development (2/3)

Security in Android App Development (2/3)

1675112374.jpg
Mohamad Abuzaid
3 months ago
Kotlin Scope Functions

Kotlin Scope Functions

1675112374.jpg
Mohamad Abuzaid
1 year ago