La inyección de dependencias (DI) es un patrón de diseño que se utiliza para implementar la inversión de control (IoC), en el que el control de la creación y la gestión de dependencias se transfiere de la aplicación a una entidad externa. Esto ayuda a crear un código más modular, comprobable y fácil de mantener. Es una técnica en la que la responsabilidad de crear objetos se transfiere a otras partes del código. Esto promueve un acoplamiento flexible, lo que hace que el código sea más modular y más fácil de gestionar.
Las clases a menudo necesitan referencias a otras clases para funcionar correctamente. Por ejemplo, considere una clase Library
que requiere una clase Book
. Estas clases necesarias se conocen como dependencias. La clase Library
depende de tener una instancia de la clase Book
para funcionar.
Hay tres formas principales para que una clase obtenga los objetos que necesita:
Library
crearía e inicializaría su propia instancia de la clase Book
.Context
y getSystemService()
, funcionan de esta manera.Library
recibiría una instancia Book
como parámetro.La tercera opción es la inyección de dependencias. Con la inyección de dependencias, se proporcionan las dependencias de una clase en lugar de que la instancia de la clase las obtenga por sí sola.
Sin DI, una Library
que crea su propia dependencia Book
podría verse así:
class Library { private Book book = new Book(); void open() { book.read(); } } public class Main { public static void main(String[] args) { Library library = new Library(); library.open(); } }
Este no es un ejemplo de DI porque la clase Library
construye su propio Book
. Esto puede ser problemático porque:
Library
y Book
están acoplados estrechamente. Una instancia de Library
utiliza un tipo de Book
, lo que dificulta el uso de subclases o implementaciones alternativas.Book
hace que las pruebas sean más desafiantes. Library
usa una instancia real de Book
, lo que evita el uso de dobles de prueba para modificar Book
para diferentes casos de prueba. Con DI, en lugar de que cada instancia de Library
construya su propio objeto Book
, recibe un objeto Book
como parámetro en su constructor:
class Library { private Book book; Library(Book book) { this.book = book; } void open() { book.read(); } } public class Main { public static void main(String[] args) { Book book = new Book(); Library library = new Library(book); library.open(); }
La función principal utiliza Library
. Dado que Library
depende de Book
, la aplicación crea una instancia de Book
y luego la utiliza para construir una instancia de Library
. Los beneficios de este enfoque basado en DI son:
Library
: puedes pasar diferentes implementaciones de Book
a Library
. Por ejemplo, puedes definir una nueva subclase de Book
llamada EBook
que quieres que Library
use. Con DI, simplemente pasas una instancia de EBook
a Library
y funciona sin más cambios.Library
: puedes pasar pruebas dobles para probar diferentes escenarios. Considere un escenario en el que una clase NotificationService
depende de una clase Notification
. Sin DI, NotificationService
crea directamente una instancia de Notification
, lo que dificulta el uso de diferentes tipos de notificaciones o la prueba del servicio con varias implementaciones de notificaciones.
Para ilustrar DI, refactoricemos este ejemplo:
interface Notification { void send(); } class EmailNotification implements Notification { @Override public void send() { // Send email notification } } class SMSNotification implements Notification { @Override public void send() { // Send SMS notification } } class NotificationService { void sendNotification(Notification notification) { notification.send(); } }
Ahora, NotificationService
depende de la interfaz Notification
en lugar de una clase específica. Esto permite que se utilicen de forma intercambiable distintas implementaciones de Notification
. Puede configurar la implementación que desea utilizar a través del método sendNotification
:
NotificationService service = new NotificationService(); service.sendNotification(new EmailNotification()); service.sendNotification(new SMSNotification());
Hay tres tipos principales de DI:
class NotificationService { private final Notification notification; public NotificationService(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(new EmailNotification()); service.sendNotification(); } }
3. Inyección de campo (o inyección de setter) : el sistema crea instancias de ciertas clases del marco de Android, como actividades y fragmentos, por lo que no es posible la inyección de constructor. Con la inyección de campo, las dependencias se crean después de que se crea la clase.
class NotificationService { private Notification notification; public Notification getNotification() { return notification; } public void setNotification(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(); service.setNotification(new EmailNotification()); service.sendNotification(); } }
4. Inyección de métodos : las dependencias se proporcionan a través de métodos, a menudo utilizando la anotación @Inject
.
En el ejemplo anterior, creaste, proporcionaste y administraste manualmente las dependencias de diferentes clases sin usar una biblioteca. Este enfoque se conoce como inyección de dependencias manual. Si bien funciona para casos simples, se vuelve engorroso a medida que aumenta la cantidad de dependencias y clases. La inyección de dependencias manual tiene varias desventajas:
Las bibliotecas pueden automatizar este proceso creando y proporcionando dependencias. Estas bibliotecas se dividen en dos categorías:
Dagger es una biblioteca de inyección de dependencias popular para Java, Kotlin y Android, mantenida por Google. Dagger simplifica la inyección de dependencias en su aplicación al crear y administrar el gráfico de dependencias por usted. Proporciona dependencias totalmente estáticas en tiempo de compilación, lo que soluciona muchos de los problemas de desarrollo y rendimiento asociados con soluciones basadas en reflexión como Guice.
Estos marcos conectan dependencias en tiempo de ejecución:
Estos marcos generan código para conectar dependencias en tiempo de compilación:
Una alternativa a la inyección de dependencias es el patrón localizador de servicios. Este patrón de diseño también ayuda a desacoplar las clases de sus dependencias concretas. Se crea una clase conocida como localizador de servicios que crea y almacena dependencias, y las proporciona a pedido.
object ServiceLocator { fun getProcessor(): Processor = Processor() } class Computer { private val processor = ServiceLocator.getProcessor() fun start() { processor.run() } } fun main(args: Array<String>) { val computer = Computer() computer.start() }
El patrón de localizador de servicios difiere de la inyección de dependencias en la forma en que se consumen las dependencias. Con el patrón de localizador de servicios, las clases solicitan las dependencias que necesitan; con la inyección de dependencias, la aplicación proporciona de manera proactiva los objetos requeridos.
Dagger 2 es un popular framework DI para Android. Utiliza generación de código en tiempo de compilación y es conocido por su alto rendimiento. Dagger 2 simplifica el proceso de inyección de dependencias al generar el código necesario para manejar las dependencias, reduciendo el código repetitivo y mejorando la eficiencia.
Dagger 2 es una biblioteca basada en anotaciones para la inyección de dependencias en Android. Estas son las anotaciones clave y sus propósitos:
ApiClient
para Retrofit.@Module
y @Inject
. Contiene todos los módulos y proporciona el generador para la aplicación.@Provides
pero más conciso. Dagger puede generar un gráfico de dependencias para su proyecto, lo que le permite determinar dónde obtener las dependencias cuando sea necesario. Para habilitar esto, debe crear una interfaz y anotarla con @Component
.
Dentro de la interfaz @Component
, se definen métodos que devuelven instancias de las clases que se necesitan (por ejemplo, BookRepository
). La anotación @Component
indica a Dagger que genere un contenedor con todas las dependencias necesarias para satisfacer los tipos que expone. Este contenedor se conoce como componente Dagger y contiene un gráfico de objetos que Dagger sabe cómo proporcionar junto con sus dependencias.
Consideremos un ejemplo que involucra un LibraryRepository
:
@Inject
al constructor LibraryRepository
para que Dagger sepa cómo crear una instancia de LibraryRepository
. public class LibraryRepository { private final LocalLibraryDataSource localDataSource; private final RemoteLibraryDataSource remoteDataSource; @Inject public LibraryRepository(LocalLibraryDataSource localDataSource, RemoteLibraryDataSource remoteDataSource) { this.localDataSource = localDataSource; this.remoteDataSource = remoteDataSource; } }
2. Anotar dependencias : de manera similar, anote los constructores de las dependencias ( LocalLibraryDataSource
y RemoteLibraryDataSource
) para que Dagger sepa cómo crearlas.
public class LocalLibraryDataSource { @Inject public LocalLibraryDataSource() { // Initialization code } } public class RemoteLibraryDataSource { private final LibraryService libraryService; @Inject public RemoteLibraryDataSource(LibraryService libraryService) { this.libraryService = libraryService; } }
3. Defina el componente : cree una interfaz anotada con @Component
para definir el gráfico de dependencia.
@Component public interface ApplicationComponent { LibraryRepository getLibraryRepository(); }
Cuando crea el proyecto, Dagger genera una implementación de la interfaz ApplicationComponent
para usted, normalmente llamada DaggerApplicationComponent
.
Ahora puedes usar el componente generado para obtener instancias de tus clases con sus dependencias inyectadas automáticamente:
public class MainApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.create(); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
En su actividad o fragmento, puede recuperar la instancia LibraryRepository
:
public class MainActivity extends AppCompatActivity { @Inject LibraryRepository libraryRepository; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MainApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected libraryRepository } }
1. Módulos
∘ Conceptos clave de los módulos
∘ Inclusión de módulos en componentes
2. Ámbitos
3. Componentes
4. Dependencias de componentes
5. Enlaces en tiempo de ejecución
Los módulos en Dagger 2 son clases anotadas con @Module
que proporcionan dependencias a los componentes. Contienen métodos anotados con @Provides
o @Binds
para especificar cómo crear y proporcionar dependencias. Los módulos son esenciales para organizar y administrar la creación de objetos que necesita su aplicación.
@Provides
y se utiliza cuando el módulo es una clase abstracta.Ejemplo de un módulo
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } @Provides @Singleton OkHttpClient provideOkHttpClient() { return new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build(); } }
En este ejemplo, NetworkModule
es una clase anotada con @Module
. Contiene dos métodos anotados con @Provides
que crean y devuelven instancias de Retrofit
y OkHttpClient
.
Usando @Binds
Cuando tienes una interfaz y su implementación, puedes usar @Binds
para vincular la implementación a la interfaz. Esto es más conciso que usar @Provides
.
public interface ApiService { void fetchData(); } public class ApiServiceImpl implements ApiService { @Override public void fetchData() { // Implementation } } @Module public abstract class ApiModule { @Binds abstract ApiService bindApiService(ApiServiceImpl apiServiceImpl); }
En este ejemplo, ApiModule
es una clase abstracta anotada con @Module
. El método bindApiService
está anotado con @Binds
para vincular ApiServiceImpl
a ApiService
.
Los módulos se pueden organizar en función de la funcionalidad que brindan. Por ejemplo, puede tener módulos separados para operaciones de red, operaciones de base de datos y dependencias relacionadas con la interfaz de usuario.
Ejemplo:
Retrofit
y OkHttpClient
.RoomDatabase
.ViewModel
y Presenter
.Los módulos se incluyen en los componentes para proporcionar dependencias a las clases que los necesitan. A continuación, se muestra cómo configurarlo:
Componente de aplicación.java:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
En este ejemplo, ApplicationComponent
incluye NetworkModule
y DatabaseModule
para proporcionar dependencias a la aplicación.
Los ámbitos en Dagger 2 son anotaciones que definen el ciclo de vida de las dependencias. Garantizan que se cree y comparta una única instancia de una dependencia dentro de un ámbito específico. Esto ayuda a gestionar la memoria de manera eficiente y a garantizar que las dependencias se reutilicen cuando corresponda.
1. Alcance Singleton
Definición : El ámbito @Singleton
garantiza que se cree y comparta una única instancia de una dependencia durante todo el ciclo de vida de la aplicación.
Este ámbito se utiliza normalmente para dependencias que deben compartirse en toda la aplicación, como clientes de red, instancias de bases de datos o preferencias compartidas.
Ejemplo:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
En este ejemplo, la anotación @Singleton
garantiza que las instancias de Retrofit
y Database
proporcionadas por NetworkModule
y DatabaseModule
sean singletons y se compartan en toda la aplicación.
2. Ámbito de actividad
Definición : @ActivityScope
(un ámbito personalizado) garantiza que se cree y comparta una única instancia de una dependencia dentro del ciclo de vida de una actividad.
Este alcance es útil para las dependencias que son específicas de una actividad y deben recrearse cada vez que se recrea la actividad, como presentadores o modelos de vista.
Ejemplo :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { } @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
En este ejemplo, la anotación @ActivityScope
garantiza que las dependencias proporcionadas por ActivityModule
estén limitadas al ciclo de vida de la actividad.
3. Alcance del fragmento
Definición : @FragmentScope
(otro ámbito personalizado) garantiza que se cree y comparta una única instancia de una dependencia dentro del ciclo de vida de un fragmento.
Caso de uso: este ámbito es útil para las dependencias que son específicas de un fragmento y deben recrearse cada vez que se recrea el fragmento, como presentadores o modelos de vista específicos del fragmento.
Ejemplo :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScope { } @FragmentScope @Component(dependencies = ActivityComponent.class, modules = FragmentModule.class) public interface FragmentComponent { void inject(MyFragment myFragment); }
En este ejemplo, la anotación @FragmentScope
garantiza que las dependencias proporcionadas por FragmentModule
tengan como alcance el ciclo de vida del fragmento.
Las dependencias de componentes permiten que un componente dependa de otro, lo que permite la reutilización de dependencias. Existen dos tipos principales de dependencias de componentes:
1. Componente de aplicación
Definición : El componente de aplicación proporciona dependencias que son necesarias en toda la aplicación. Normalmente, se le asigna el alcance @Singleton
para garantizar que las dependencias se compartan en toda la aplicación.
Este componente se utiliza para dependencias que deben estar disponibles globalmente, como clientes de red, instancias de bases de datos o preferencias compartidas.
Ejemplo :
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
En este ejemplo, ApplicationComponent
es responsable de proporcionar instancias de Retrofit
y Database
, que se comparten en toda la aplicación.
2. Componente de actividad
Definición : El componente de actividad proporciona dependencias que son necesarias dentro de una actividad específica. Normalmente, tiene un alcance personalizado, como @ActivityScope
, para garantizar que las dependencias se vuelvan a crear cada vez que se vuelve a crear la actividad.
Este componente se utiliza para dependencias que son específicas de una actividad, como presentadores o modelos de vista.
Ejemplo :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
En este ejemplo, ActivityComponent
depende de ApplicationComponent
y proporciona dependencias específicas de MainActivity
.
Las dependencias de componentes permiten que un componente dependa de otro, lo que permite la reutilización de dependencias. Existen dos tipos principales de dependencias de componentes:
1. Subcomponentes:
Un subcomponente es un componente secundario de otro componente y puede acceder a las dependencias de su componente principal. Los subcomponentes se definen dentro del componente principal y pueden heredar su alcance.
Ejemplo :
@ActivityScope @Subcomponent(modules = ActivityModule.class) public interface ActivitySubcomponent { void inject(MainActivity mainActivity); }
En este ejemplo, ActivitySubcomponent
es un subcomponente del componente principal y puede acceder a sus dependencias.
2. Atributo de dependencia
Esto permite que un componente dependa de otro componente sin ser un subcomponente. El componente dependiente puede acceder a las dependencias proporcionadas por el componente principal.
Ejemplo :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
En este ejemplo, ActivityComponent
depende de ApplicationComponent
y puede acceder a sus dependencias.
Los enlaces de tiempo de ejecución en Dagger 2 se refieren a la provisión de dependencias que se crean y administran en tiempo de ejecución, según el contexto en el que se necesitan.
1. Contexto de aplicación
Definición : El contexto de la aplicación es un contexto que está vinculado al ciclo de vida de toda la aplicación. Se utiliza para dependencias que deben durar tanto como la aplicación misma.
Dependencias que se comparten en toda la aplicación y que no es necesario volver a crear para cada actividad o fragmento. Algunos ejemplos son los clientes de red, las instancias de base de datos y las preferencias compartidas.
Ejemplo :
@Module public class AppModule { private final Application application; public AppModule(Application application) { this.application = application; } @Provides @Singleton Application provideApplication() { return application; } @Provides @Singleton Context provideApplicationContext() { return application.getApplicationContext(); } }
En este ejemplo, AppModule
proporciona el contexto de la aplicación como una dependencia singleton. El método provideApplicationContext
garantiza que el contexto proporcionado esté vinculado al ciclo de vida de la aplicación.
2. Contexto de la actividad
Definición : El contexto de actividad es un contexto que está vinculado al ciclo de vida de una actividad específica. Se utiliza para dependencias que deben durar tanto como la actividad misma.
Dependencias que son específicas de una actividad y que se deben volver a crear cada vez que se vuelve a crear la actividad. Algunos ejemplos son los modelos de vista, los presentadores y las dependencias relacionadas con la interfaz de usuario.
Ejemplo :
@Module public class ActivityModule { private final Activity activity; public ActivityModule(Activity activity) { this.activity = activity; } @Provides @ActivityScope Activity provideActivity() { return activity; } @Provides @ActivityScope Context provideActivityContext() { return activity; } }
En este ejemplo, ActivityModule
proporciona el contexto de la actividad como una dependencia con alcance. El método provideActivityContext
garantiza que el contexto proporcionado esté vinculado al ciclo de vida de la actividad.
Para utilizar estos enlaces de tiempo de ejecución, debe incluir los módulos correspondientes en sus componentes:
Componente de aplicación :
@Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); Context getApplicationContext(); }
Componente de actividad :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); Context getActivityContext(); }
Inyección de contextos
Una vez que haya configurado sus componentes y módulos, puede inyectar los contextos en sus clases según sea necesario.
Ejemplo en una actividad :
public class MainActivity extends AppCompatActivity { @Inject Context activityContext; @Inject Context applicationContext; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ApplicationComponent appComponent = ((MyApplication) getApplication()).getApplicationComponent(); ActivityComponent activityComponent = DaggerActivityComponent.builder() .applicationComponent(appComponent) .activityModule(new ActivityModule(this)) .build(); activityComponent.inject(this); // Use the injected contexts Log.d("MainActivity", "Activity Context: " + activityContext); Log.d("MainActivity", "Application Context: " + applicationContext); } }
En este ejemplo, MainActivity
recibe tanto el contexto de la actividad como el de la aplicación a través de la inyección de dependencias. Esto permite que la actividad utilice el contexto adecuado en función de las necesidades específicas de las dependencias.
Para usar Dagger 2 en su proyecto, debe agregar las siguientes dependencias a su archivo build.gradle
:
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Reemplace 2.x
con la última versión de Dagger 2.
Cree un módulo para proporcionar dependencias. Por ejemplo, un NetworkModule
para proporcionar una instancia Retrofit
:
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } }
Cree un componente para unir el módulo y las clases que necesitan las dependencias:
@Singleton @Component(modules = {NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Utilice el componente para inyectar dependencias en sus clases. Por ejemplo, en su clase Application
:
public class MyApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.builder() .networkModule(new NetworkModule()) .build(); applicationComponent.inject(this); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
Ahora puedes usar las dependencias inyectadas en tus clases. Por ejemplo, en una Activity
:
public class MainActivity extends AppCompatActivity { @Inject Retrofit retrofit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MyApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected Retrofit instance // ... } }
Resumamos este tema: