¿Cómo funciona el alcance de la daga? Cuando aprendí a usar Dagger, el … | de Abhishek Luthra | Noviembre de 2020

Cuando aprendí a usar Dagger, el concepto de los llamados visores fue uno de los más difíciles de entender.

Hoy me doy cuenta de que la mejor manera de entender los dominios de Dagger es sumergirse en las profundidades del código generado. Suena complicado, pero te aseguro que no lo es. Bueno, es un poco complicado si lo hace usted mismo, pero no es necesario.

En esta publicación, lo guiaré a través del código que genera Dagger y explicaré el funcionamiento interno de los ámbitos.

me gustaríaKy para asegurarme de que la terminología que voy a utilizar sea clara, repasemos brevemente las construcciones de Dagger. Si eres completamente nuevo en Dagger, es posible que desees comenzar con un tutorial adecuado de Dagger y volver a esta publicación más tarde.

Dagger 2 es un marco de inyección de dependencias para Android y Java. Creado originalmente por Square, ahora es administrado por Google.

Los “bloques de construcción” básicos utilizados por Dagger 2 son los siguientes:

  • Componentes. Los componentes son “inyectores” que realizan la inyección real en los “clientes”.
  • Formularios. Los módulos son los objetos en los que se define la estructura del “gráfico de objetos” (la forma en que se instancian los “servicios”).
  • Áreas. Los ámbitos son anotaciones de Java que, cuando se utilizan, cambian la forma en que se crean instancias de “servicios” tras la inyección.

Tenga en cuenta la terminología: en esta publicación, cada vez que digo “servicio” no me refiero a la clase de servicio de Android, sino a algún objeto que se inyecta en otro objeto; cada vez que digo “cliente” me refiero a un objeto que se inyecta con otro objeto.

Para comprender mejor qué hacen los ámbitos, veamos qué sucede cuando entro en “servicios sin ámbito”.

Para simplificar, inyectaré esta clase:

public class UnScopedService {
public UnScopedService() {
}
}

UnScopedService se declara como un servicio (es decir, se declara como “inyectable”) en este módulo:

@Module
public class DemoModule {@Provides
public UnScopedService provideUnscopedService() {
return new UnScopedService();
}}

Este es el componente que hace uso de DemoModule y declara MainActivity como cliente (es decir, declara que puede inyectar servicios en MainActivity):

@Component(modules = {DemoModule.class})
public interface DemoComponent {
void inject(MainActivity mainActivity);
}

Y el cliente también es muy básico (el contenido de activity_main.xml es irrelevante para nuestra discusión actual):

public class MainActivity extends AppCompatActivity {

@Inject
UnScopedService mUnScopedService;
private DemoComponent mDemoComponent;

@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
getDemoComponent().inject(this);
super.onCreate(savedInstanceState, persistentState);
setContentView(R.layout.activity_main);
}
private DemoComponent getDemoComponent() {
if (mDemoComponent == null) {
mDemoComponent = DaggerDemoComponent.builder().demoModule(new DemoModule()).build();
}
return mDemoComponent;
}
}

Después de construir este proyecto, puedo echar un vistazo al código real que realiza la inyección.

Partiendo del método inject (MainActivity) de DemoComponent, sigo los usos e implementaciones de una serie de clases y finalmente llego a la clase MainActivity_MembersInjector:

public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {    private final Provider<UnScopedService> mUnScopedServiceProvider;    public MainActivity_MembersInjector(Provider<UnScopedService> mUnScopedServiceProvider) {
assert mUnScopedServiceProvider != null;
this.mUnScopedServiceProvider = mUnScopedServiceProvider;
}
public static MembersInjector<MainActivity> create(
Provider<UnScopedService> mUnScopedServiceProvider) {
return new MainActivity_MembersInjector(mUnScopedServiceProvider);
}
@Override
public void injectMembers(MainActivity instance) {
if (instance == null) {
throw new NullPointerException("Cannot inject members into a null reference");
}
instance.mUnScopedService = mUnScopedServiceProvider.get();
}
public static void injectMUnScopedService(
MainActivity instance, Provider<UnScopedService> mUnScopedServiceProvider) {
instance.mUnScopedService = mUnScopedServiceProvider.get();
}
}

La última línea del método injectMembers es donde tiene lugar la inyección real de UnScopedService. Tenga en cuenta que el servicio no se crea aquí, sino que se recupera de una instancia de la interfaz del proveedor.

Para saber qué implementación de proveedor se utilizó, busco los usos del método MainActivity_MembersInjector # create (). Es llamado por DaggerDemoComponent:

public final class DaggerDemoComponent implements DemoComponent {
private Provider<UnScopedService> provideUnscopedServiceProvider;
private MembersInjector<MainActivity> mainActivityMembersInjector; private DaggerDemoComponent(Builder builder) {
assert builder != null;
initialize(builder);
}
public static Builder builder() {
return new Builder();
}
public static DemoComponent create() {
return new Builder().build();
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.provideUnscopedServiceProvider =
DemoModule_ProvideUnscopedServiceFactory.create(builder.demoModule);
this.mainActivityMembersInjector =
MainActivity_MembersInjector.create(provideUnscopedServiceProvider);
}
@Override
public void inject(MainActivity mainActivity) {
mainActivityMembersInjector.injectMembers(mainActivity);
}
public static final class Builder {
private DemoModule demoModule;
private Builder() {} public DemoComponent build() {
if (demoModule == null) {
this.demoModule = new DemoModule();
}
return new DaggerDemoComponent(this);
}
public Builder demoModule(DemoModule demoModule) {
this.demoModule = Preconditions.checkNotNull(demoModule);
return this;
}
}
}

Como puede ver en el método de inicialización, DemoModule_ProvideUnscopedServiceFactory se utiliza como proveedor de instancias de UnScopedService.

Veamos qué está sucediendo dentro de esta clase:

public final class DemoModule_ProvideUnscopedServiceFactory implements Factory<UnScopedService> {
private final DemoModule module;
public DemoModule_ProvideUnscopedServiceFactory(DemoModule module) {
assert module != null;
this.module = module;
}
@Override
public UnScopedService get() {
return Preconditions.checkNotNull(
module.provideUnscopedService(),
"Cannot return null from a [email protected] @Provides method");
}
public static Factory<UnScopedService> create(DemoModule module) {
return new DemoModule_ProvideUnscopedServiceFactory(module);
}
}

El código en la última línea para obtener el método simplemente obtiene una instancia UnScopedService de DemoModule, que es la clase que yo mismo definí. Parece que cuando necesita inyectar un servicio sin ámbito, el código generado por Dagger 2 simplemente delega la instancia de ese servicio a un método en la clase Module definida por el usuario.

Imagine que desea utilizar la misma instancia de DemoComponent para inyectar UnScopedService en varios campos. Debido a que cada inyección en un campo da como resultado la obtención de una nueva instancia del formulario, cada campo apuntará a un servicio diferente.

Recuerde este punto porque, como veremos en breve, es exactamente este mecanismo de instanciar un nuevo servicio para cada campo el que afectará a los dominios.

Dagger 2 reconoce un único alcance predeterminado: @Singleton. Permítame mostrarle cómo aplicar el alcance al servicio.

Así que profundizaré en lo que sucede cuando Dagger inyecta el servicio anotado con el alcance @Singleton.

El servicio Scope también será muy sencillo:

public class SingletonScopedService {    public SingletonScopedService() {
}

}

Tenga en cuenta que el servicio en sí no tiene dependencias en el framework Dagger 2 y, por lo tanto, no es consciente de su alcance. Incluyo el nombre del alcance en el nombre del servicio solo con el propósito de este tutorial, pero no debería hacer lo mismo en sus proyectos.

Para asignar un alcance al servicio, agregamos la anotación @Singleton a su método de proveedor en la clase DemoModule:

@Module
public class DemoModule {
@Provides
public UnScopedService provideUnscopedService() {
return new UnScopedService();
}
@Provides
@Singleton
public SingletonScopedService singletonScopedService() {
return new SingletonScopedService();
}
}

Dagger 2 aplica una regla según la cual para inyectar servicios con ámbito, el componente de inyección también debe anotarse con el ámbito correspondiente:

@Singleton
@Component(modules = {DemoModule.class})
public interface DemoComponent {
void inject(MainActivity mainActivity);
}

Y el cliente que utiliza ambos servicios se convierte en:

public class MainActivity extends AppCompatActivity {    @Inject
UnScopedService mUnScopedService;
@Inject
SingletonScopedService mSingletonScopedService;
private DemoComponent mDemoComponent; @Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
getDemoComponent().inject(this);
super.onCreate(savedInstanceState, persistentState);
setContentView(R.layout.activity_main);
}
private DemoComponent getDemoComponent() {
if (mDemoComponent == null) {
mDemoComponent = DaggerDemoComponent.builder().demoModule(new DemoModule()).build();
}
return mDemoComponent;
}
}

Ahora es el momento de rastrear los métodos y clases involucrados en la inyección de SingletonScopedService exactamente de la misma manera que lo hice anteriormente para UnScopedService.

Cuando hago esto, encuentro que la única diferencia en el tratamiento de estos servicios está en la clase DaggerDemoComponent:

public final class DaggerDemoComponent implements DemoComponent {
private Provider<UnScopedService> provideUnscopedServiceProvider;
private Provider<SingletonScopedService> singletonScopedServiceProvider; private MembersInjector<MainActivity> mainActivityMembersInjector; private DaggerDemoComponent(Builder builder) {
assert builder != null;
initialize(builder);
}
public static Builder builder() {
return new Builder();
}
public static DemoComponent create() {
return new Builder().build();
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.provideUnscopedServiceProvider =
DemoModule_ProvideUnscopedServiceFactory.create(builder.demoModule);
this.singletonScopedServiceProvider =
DoubleCheck.provider(DemoModule_SingletonScopedServiceFactory.create(builder.demoModule));
this.mainActivityMembersInjector =
MainActivity_MembersInjector.create(
provideUnscopedServiceProvider, singletonScopedServiceProvider);
}
@Override
public void inject(MainActivity mainActivity) {
mainActivityMembersInjector.injectMembers(mainActivity);
}
public static final class Builder {
private DemoModule demoModule;
private Builder() {} public DemoComponent build() {
if (demoModule == null) {
this.demoModule = new DemoModule();
}
return new DaggerDemoComponent(this);
}
public Builder demoModule(DemoModule demoModule) {
this.demoModule = Preconditions.checkNotNull(demoModule);
return this;
}
}
}

Eche un vistazo de cerca al código en el método de inicialización. Mientras que DemoModule_ProvideUnscopedServiceFactory se usa directamente en MainActivity_MembersInjector, DemoModule_SingletonScopedServiceFactory está envuelto en DoubleCheck (que es otra implementación de la interfaz del proveedor).

Dado que esta es la única diferencia entre los mecanismos de inyección UnScopedService y SignletonScopedService de Dagger, cualquier diferencia funcional introducida por el alcance @Singleton se debe a la clase Decoración por DoubleCheck.

public final class DoubleCheck<T> implements Provider<T>, Lazy<T> {
private static final Object UNINITIALIZED = new Object();
private volatile Provider<T> provider;
private volatile Object instance = UNINITIALIZED;
private DoubleCheck(Provider<T> provider) {
assert provider != null;
this.provider = provider;
}
@SuppressWarnings("unchecked") // cast only happens when result comes from the provider
@Override
public T get() {
Object result = instance;
if (result == UNINITIALIZED) {
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
Object currentInstance = instance;
if (currentInstance != UNINITIALIZED && currentInstance != result) {
throw new IllegalStateException("Scoped provider was invoked recursively returning "
+ "different results: " + currentInstance + " & " + result + ". This is likely "
+ "due to a circular dependency.");
}
instance = result;
provider = null;
}
}
}
return (T) result;
}
public static <T> Provider<T> provider(Provider<T> delegate) {
checkNotNull(delegate);
if (delegate instanceof DoubleCheck) {
return delegate;
}
return new DoubleCheck<T>(delegate);
}
public static <T> Lazy<T> lazy(Provider<T> provider) {
if (provider instanceof Lazy) {
@SuppressWarnings("unchecked") final Lazy<T> lazy = (Lazy<T>) provider;
return lazy;
}
return new DoubleCheck<T>(checkNotNull(provider));
}
}

Si bien este código suena complicado, es el enfoque estándar para garantizar que solo se devuelva una instancia del servicio. Esta clase almacena en caché el servicio devuelto por la primera llamada al método get () y luego devuelve el mismo servicio en llamadas posteriores.

Esto convierte a DoubleCheck en un decorador de almacenamiento en caché: es un proveedor que envuelve a otro proveedor y almacena en caché la instancia que devuelve.

Vale la pena señalar que la implementación del método get () en DoubleCheck usa un lenguaje de “doble verificación” seguro para subprocesos. Esto puede ser importante si tiene la intención de utilizar DemoComponent desde varios subprocesos. Sin embargo, en la práctica, nunca he visto una razón para inyectar en un hilo que no sea de UI en Android.

¿Qué pasaría si usara la misma instancia de DemoComponent para inyectar SingletonScopedService en varios campos?

Bueno, dado que el primer servicio inyectado se almacena en caché, todos los campos terminarían teniendo una referencia al mismo objeto.

Este resultado es diferente de lo que sucedería con campos de tipo UnScopedService (cada uno de los cuales apuntaría a un servicio diferente) y esta es la única diferencia introducida por la anotación de alcance @Singleton.

Inyección de alcance definida por el usuario:

Además de un alcance @Singleton predeterminado, puedo definir mis alcances de la siguiente manera (este código debe estar en un archivo llamado CustomScope.java):

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomScope {
}

Una vez que el alcance está definido de esta manera, puedo usarlo en mi proyecto. Por ejemplo, podría reemplazar todas las apariciones de la anotación @Singleton con la anotación @CustomScope.

Si rastreo el código generado automáticamente como antes, encontraré que Dagger trata a @CustomScope exactamente de la misma manera que trata el alcance de @Singleton. Esto significa que “todos los ámbitos se construyen de la misma manera” y los nombres de dominio son irrelevantes desde el punto de vista de Dagger (aunque le animo a que los nombre descriptivamente para facilitar la lectura).

  • Cada vez que el mismo componente inyecta un servicio sin ámbito, se crea una nueva instancia de un servicio.
  • La primera vez que se inyecta un servicio de ámbito @Singleton, se crea una instancia y se almacena en caché dentro del componente de inyección, por lo que se utilizará exactamente la misma instancia al inyectar otros campos del mismo tipo desde el mismo componente.
  • Los ámbitos personalizados definidos por el usuario son funcionalmente equivalentes a un ámbito @Singleton predefinido
  • Inyectar servicios de ámbito es seguro para subprocesos.

Esto es lo que realmente hacen los osciloscopios.

Dado que los componentes almacenan en caché y reutilizan la misma instancia de servicio de ámbito en el momento de la inyección (independientemente del nombre del ámbito), la cuestión de “si el servicio inyectado es singleton” se puede reducir a una cuestión de “si instancias de un servicio se han inyectado con el mismo componente ”.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *