Dagger- ¿Deberíamos crear cada componente y módulo para cada Actividad / Fragmento?

85

He estado trabajando con dagger2 por un tiempo. Y me confundí si debía crear un componente / módulo propio para cada Actividad / Fragmento. Ayúdame a aclarar esto:

Por ejemplo, tenemos una aplicación y la aplicación tiene alrededor de 50 pantallas. Implementaremos el código siguiendo el patrón MVP y Dagger2 para DI. Supongamos que tenemos 50 actividades y 50 presentadores.

En mi opinión, normalmente deberíamos organizar el código así:

  1. Cree un AppComponent y AppModule que proporcionará todos los objetos que se utilizarán mientras la aplicación esté abierta.

    @Module
    public class AppModule {
    
        private final MyApplicationClass application;
    
        public AppModule(MyApplicationClass application) {
            this.application = application;
        }
    
        @Provides
        @Singleton
        Context provideApplicationContext() {
            return this.application;
        }
    
        //... and many other providers 
    
    }
    
    @Singleton
    @Component( modules = { AppModule.class } )
    public interface AppComponent {
    
        Context getAppContext();
    
        Activity1Component plus(Activity1Module module);
        Activity2Component plus(Activity2Module module);
    
        //... plus 48 methods for 48 other activities. Suppose that we don't have any other Scope (like UserScope after user login, ....)
    
    }
    
  2. Crear ActivityScope:

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Cree un componente y un módulo para cada actividad. Normalmente los pondré como clases estáticas dentro de la clase Activity:

    @Module
    public class Activity1Module {
    
        public LoginModule() {
        }
        @Provides
        @ActivityScope
        Activity1Presenter provideActivity1Presenter(Context context, /*...some other params*/){
            return new Activity1PresenterImpl(context, /*...some other params*/);
        }
    
    }
    
    @ActivityScope
    @Subcomponent( modules = { Activity1Module.class } )
    public interface Activity1Component {
        void inject(Activity1 activity); // inject Presenter to the Activity
    }
    
    // .... Same with 49 remaining modules and components.
    

Esos son solo ejemplos muy simples para mostrar cómo implementaría esto.

Pero un amigo mío me acaba de dar otra implementación:

  1. Cree PresenterModule que proporcionará a todos los presentadores:

    @Module
    public class AppPresenterModule {
    
        @Provides
        Activity1Presenter provideActivity1Presentor(Context context, /*...some other params*/){
            return new Activity1PresenterImpl(context, /*...some other params*/);
        }
    
        @Provides
        Activity2Presenter provideActivity2Presentor(Context context, /*...some other params*/){
            return new Activity2PresenterImpl(context, /*...some other params*/);
        }
    
        //... same with 48 other presenters.
    
    }
    
  2. Cree AppModule y AppComponent:

    @Module
    public class AppModule {
    
        private final MyApplicationClass application;
    
        public AppModule(MyApplicationClass application) {
            this.application = application;
        }
    
        @Provides
        @Singleton
        Context provideApplicationContext() {
            return this.application;
        }
    
        //... and many other provides 
    
    }
    
    @Singleton
    @Component(
            modules = { AppModule.class,  AppPresenterModule.class }
    )
    public interface AppComponent {
    
        Context getAppContext();
    
        public void inject(Activity1 activity);
        public void inject(Activity2 activity);
    
        //... and 48 other methods for 48 other activities. Suppose that we don't have any other Scope (like UserScope after user login, ....)
    
    }
    

Su explicación es: no tiene que crear componentes y módulos para cada actividad. Creo que la idea de mi amigo no es buena en absoluto, pero corríjame si me equivoco. Estas son las razones:

  1. Muchas pérdidas de memoria :

    • La aplicación creará 50 presentadores incluso si el usuario solo tiene 2 actividades abiertas.
    • Una vez que el usuario cierra una actividad, su presentador seguirá siendo
  2. ¿Qué sucede si quiero crear dos instancias de una actividad? (cómo puede crear dos presentadores)

  3. La aplicación tardará mucho en inicializarse (porque tiene que crear muchos presentadores, objetos, ...)

Perdón por una publicación larga, pero por favor ayúdame a aclarar esto para mí y para mi amigo, no puedo convencerlo. Tus comentarios serán muy apreciados.

/ ------------------------------------------------- ---------------------- /

Edite después de hacer una demostración.

Primero, gracias por la respuesta de @pandawarrior. Debería haber creado una demostración antes de hacer esta pregunta. Espero que mi conclusión aquí pueda ayudar a alguien más.

  1. Lo que ha hecho mi amigo no causa pérdidas de memoria a menos que ponga algún Scope en los métodos Provides. (Por ejemplo @Singleton o @UserScope, ...)
  2. Podemos crear muchos presentadores, si el método Provides no tiene ningún alcance. (Entonces, mi segundo punto también está mal)
  3. Dagger creará los presentadores solo cuando sean necesarios. (Entonces, la aplicación no tardará mucho en inicializarse, estaba confundido por Lazy Injection)

Entonces, todas las razones que he dicho anteriormente son en su mayoría incorrectas. Pero eso no significa que debamos seguir la idea de mi amigo, por dos razones:

  1. No es bueno para la arquitectura de la fuente, cuando inicia a todos los presentadores en módulo / componente. (Viola el principio de segregación de interfaz , quizás también el principio de responsabilidad única).

  2. Cuando creamos un componente de alcance, sabremos cuándo se crea y cuándo se destruye, lo cual es un gran beneficio para evitar pérdidas de memoria. Entonces, para cada actividad deberíamos crear un componente con un @ActivityScope. Imaginemos, con la implementación de mis amigos, que nos olvidamos de poner algo de Scope en el método Provider => ocurrirán pérdidas de memoria.

En mi opinión, con una aplicación pequeña (pocas pantallas sin muchas dependencias o con dependencias similares), podríamos aplicar la idea de mis amigos, pero por supuesto que no es recomendable.

Prefiero leer más sobre: ¿Qué determina el ciclo de vida de un componente (gráfico de objeto) en Dagger 2? Alcance de la actividad de Dagger2, ¿cuántos módulos / componentes necesito?

Y una nota más: si desea ver cuándo se destruye el objeto, puede llamar a los del método juntos y el GC se ejecutará inmediatamente:

    System.runFinalization();
    System.gc();

Si usa solo uno de estos métodos, GC se ejecutará más tarde y puede obtener resultados incorrectos.

Sr. Mike
fuente

Respuestas:

85

Declarar un módulo separado para cada uno Activityno es una buena idea en absoluto. Declarando un componente separado para cada unoActivity es aún peor. El razonamiento detrás de esto es muy simple: realmente no necesita todos estos módulos / componentes (como ya lo ha visto usted mismo).

Sin embargo, tener un solo componente que esté vinculado al Applicationciclo de vida y usarlo para inyección en todos Activitiestampoco es la solución óptima (este es el enfoque de su amigo). No es óptimo porque:

  1. Lo restringe a un solo alcance ( @Singletono uno personalizado)
  2. El único alcance al que está restringido hace que los objetos inyectados sean "únicos de aplicación", por lo que los errores en el alcance o el uso incorrecto de los objetos del alcance pueden causar fácilmente pérdidas de memoria global
  3. También querrá usar Dagger2 para inyectar Services, pero Servicespuede requerir objetos diferentes Activities(por ejemplo Services, no necesita presentadores, no los tengo FragmentManager, etc.). Al usar un solo componente, pierde la flexibilidad de definir diferentes gráficos de objetos para diferentes componentes.

Entonces, un componente por Activityes excesivo, pero un solo componente para toda la aplicación no es lo suficientemente flexible. La solución óptima está entre estos extremos (como suele ser).

Utilizo el siguiente enfoque:

  1. Componente de "aplicación" único que proporciona objetos "globales" (por ejemplo, objetos que tienen un estado global que se comparte entre todos los componentes de la aplicación). Instanciado en Application.
  2. Subcomponente "controlador" del componente "aplicación" que proporciona objetos que son requeridos por todos los "controladores" orientados al usuario (en mi arquitectura son Activitiesy Fragments). Instanciado en cada ActivityyFragment .
  3. Subcomponente de "servicio" del componente de "aplicación" que proporciona objetos que son requeridos por todos Services. Instanciado en cada uno Service.

A continuación se muestra un ejemplo de cómo podría implementar el mismo enfoque.


Editar julio de 2017

Publiqué un video tutorial que muestra cómo estructurar el código de inyección de dependencia de Dagger en la aplicación de Android : Android Dagger for Professionals Tutorial .


Editar febrero de 2018

Publiqué un curso completo sobre inyección de dependencias en Android .

En este curso explico la teoría de la inyección de dependencia y muestro cómo surge de forma natural en la aplicación de Android. Luego demuestro cómo las construcciones de Dagger encajan en el esquema de inyección de dependencia general.

Si toma este curso, comprenderá por qué la idea de tener una definición separada de módulo / componente para cada Actividad / Fragmento es básicamente defectuosa en la forma más fundamental.

Este enfoque hace que la estructura de la capa de presentación del conjunto de clases "Funcional" se refleje en la estructura del conjunto de clases de "Construcción", acoplándolos así. Esto va en contra del objetivo principal de la inyección de dependencias, que es mantener separados los conjuntos de clases "Construcción" y "Funcional".


Ámbito de aplicación:

@ApplicationScope
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {

    // Each subcomponent can depend on more than one module
    ControllerComponent newControllerComponent(ControllerModule module);
    ServiceComponent newServiceComponent(ServiceModule module);

}


@Module
public class ApplicationModule {

    private final Application mApplication;

    public ApplicationModule(Application application) {
        mApplication = application;
    }

    @Provides
    @ApplicationScope
    Application applicationContext() {
        return mApplication;
    }

    @Provides
    @ApplicationScope
    SharedPreferences sharedPreferences() {
        return mApplication.getSharedPreferences(Constants.PREFERENCES_FILE, Context.MODE_PRIVATE);
    }

    @Provides
    @ApplicationScope
    SettingsManager settingsManager(SharedPreferences sharedPreferences) {
        return new SettingsManager(sharedPreferences);
    }
}

Alcance del controlador:

@ControllerScope
@Subcomponent(modules = {ControllerModule.class})
public interface ControllerComponent {

    void inject(CustomActivity customActivity); // add more activities if needed

    void inject(CustomFragment customFragment); // add more fragments if needed

    void inject(CustomDialogFragment customDialogFragment); // add more dialogs if needed

}



@Module
public class ControllerModule {

    private Activity mActivity;
    private FragmentManager mFragmentManager;

    public ControllerModule(Activity activity, FragmentManager fragmentManager) {
        mActivity = activity;
        mFragmentManager = fragmentManager;
    }

    @Provides
    @ControllerScope
    Context context() {
        return mActivity;
    }

    @Provides
    @ControllerScope
    Activity activity() {
        return mActivity;
    }

    @Provides
    @ControllerScope
    DialogsManager dialogsManager(FragmentManager fragmentManager) {
        return new DialogsManager(fragmentManager);
    }

    // @Provides for presenters can be declared here, or in a standalone PresentersModule (which is better)
}

Y luego en Activity:

public class CustomActivity extends AppCompatActivity {

    @Inject DialogsManager mDialogsManager;

    private ControllerComponent mControllerComponent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getControllerComponent().inject(this);

    }

    private ControllerComponent getControllerComponent() {
        if (mControllerComponent == null) {

            mControllerComponent = ((MyApplication)getApplication()).getApplicationComponent()
                    .newControllerComponent(new ControllerModule(this, getSupportFragmentManager()));
        }

        return mControllerComponent;
    }
}

Información adicional sobre la inyección de dependencia:

Dagger 2 Scopes desmitificados

Inyección de dependencia en Android

Vasiliy
fuente
1
Gracias @vasiliy por compartir tu opinión. Así es exactamente como lo usaría y la estrategia que sigo actualmente. En caso de un patrón MVP, el referido ControllerModulecreará uno nuevo Presentery luego se inyecta el presentador en el Activityo Fragment. ¿Alguna opinión sólida a favor o en contra de esto?
Wahib Ul Haq
@Vasiliy, leí todo tu artículo y descubrí que tal vez no consideraste a los interactores y presentadores en el mecanismo. ¿ControllerModule proporcionará toda la dependencia de interactuadores y presentadores ? Por favor, dé una pequeña pista en caso de que me haya perdido algo.
iamcrypticcoder
@ mahbub.kuet, si entiendo a qué te refieres con "interactores" y "presentadores", ControllerComponentdebería inyectarlos. Ya sea que los conecte al interior ControllerModuleo introduzca un módulo adicional, depende de usted. En aplicaciones reales, recomiendo usar un enfoque de múltiples módulos por componente en lugar de poner todo en un solo módulo. Aquí hay un ejemplo de ApplicationComponent, pero el controlador será el mismo: github.com/techyourchance/idocare-android/tree/master/app/src/…
Vasiliy
2
@ Mr.Hyde, en general sí, pero luego tendrás que declarar explícitamente en ApplicationComponenttodas las dependencias que ControllerComponentpuedas usar. Además, el recuento de métodos del código generado será mayor. Todavía no he encontrado una buena razón para usar componentes dependientes.
Vasiliy
1
Estoy usando este enfoque en todos mis proyectos hoy y explícitamente no uso nada del dagger.androidpaquete porque lo encuentro mal motivado. Por lo tanto, este ejemplo todavía está muy actualizado y sigue siendo la mejor manera de hacer DI en Android en mi humilde opinión.
Vasiliy
15

Algunos de los mejores ejemplos de cómo organizar sus componentes, módulos y paquetes se pueden encontrar en el repositorio de Google Android Architecture Blueprints Github aquí .

Si examina el código fuente allí, puede ver que hay un solo Componente de ámbito de aplicación (con un ciclo de vida de la duración de toda la aplicación) y luego los Componentes de ámbito de actividad separados para la Actividad y el Fragmento correspondiente a una funcionalidad dada en un proyecto. Por ejemplo, existen los siguientes paquetes:

addedittask
taskdetail
tasks

Dentro de cada paquete hay un módulo, componente, presentador, etc. Por ejemplo, dentro taskdetailestán las siguientes clases:

TaskDetailActivity.java
TaskDetailComponent.java
TaskDetailContract.java
TaskDetailFragment.java
TaskDetailPresenter.java
TaskDetailPresenterModule.java

La ventaja de organizarse de esta manera (en lugar de agrupar todas las actividades en un componente o módulo) es que puede aprovechar los modificadores de accesibilidad de Java y cumplir con el elemento 13. En otras palabras, las clases agrupadas funcionalmente estarán en el mismo empaqueta y se puede aprovechar protectedy package-private modificadores de accesibilidad para evitar usos no deseados de sus clases.

David Rawson
fuente
1
este es también mi enfoque preferido. No me gusta que las actividades / fragmentos tengan acceso a cosas que no deberían.
Joao Sousa
3

La primera opción crea un componente de subámbito para cada actividad, donde la actividad es capaz de crear componentes de subámbito que solo proporcionan la dependencia (presentador) para esa actividad en particular.

La segunda opción crea un @Singletoncomponente único que puede proporcionar a los presentadores como dependencias sin ámbito, lo que significa que cuando accede a ellos, crea una nueva instancia del presentador cada vez. (No, no crea una nueva instancia hasta que solicite una).


Técnicamente, ninguno de los enfoques es peor que el otro. El primer enfoque no separa a los presentadores por función, sino por capa.

He usado ambos, ambos funcionan y ambos tienen sentido.

La única desventaja de la primera solución (si está usando en @Component(dependencies={...}lugar de @Subcomponent) es que debe asegurarse de que no sea la Actividad la que crea su propio módulo internamente, porque entonces no puede reemplazar las implementaciones del método del módulo con simulaciones. Por otra parte, si usa la inyección de constructor en lugar de la inyección de campo, puede crear la clase directamente con el constructor, dándole directamente simulaciones.

EpicPandaForce
fuente
1

Úselo en Provider<"your component's name">lugar de la implementación de componentes simples para evitar pérdidas de memoria y la creación de toneladas de componentes inútiles. Por lo tanto, sus componentes serán creados por lazy cuando llame al método get () ya que no proporciona una instancia del componente, sino solo el proveedor. Por lo tanto, su presentador se aplicará si se llamó a .get () del proveedor. Lea sobre Proveedor aquí y aplique esto. ( Documentación oficial de Dagger )


Y otra excelente manera es usar multibinding. De acuerdo con él, debe vincular a sus presentadores en un mapa y crearlos a través de proveedores cuando lo necesite. ( aquí hay documentos sobre multienlace )

Konstantin Levitskiy
fuente
-5

Tu amigo tiene razón, realmente no tienes que crear componentes y módulos para cada actividad. Se supone que Dagger lo ayuda a reducir el código desordenado y hace que sus actividades de Android sean más limpias al delegar instancias de clases a los módulos en lugar de instanciarlas en el método onCreate de Actividades.

Normalmente lo haremos así

public class MainActivity extends AppCompatActivity {


Presenter1 mPresenter1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mPresenter1 = new Presenter1(); // you instantiate mPresentation1 in onCreate, imagine if there are 5, 10, 20... of objects for you to instantiate.
}

}

Tu haces esto en su lugar

public class MainActivity extends AppCompatActivity {

@Inject
Presenter1 mPresenter1; // the Dagger module take cares of instantiation for your

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    injectThisActivity();
}

private void injectThisActivity() {
    MainApplication.get(this)
            .getMainComponent()
            .inject(this);
}}

Entonces, escribir demasiadas cosas frustra el propósito de la daga, ¿no? Prefiero crear una instancia de mis presentadores en Actividades si tengo que crear módulos y componentes para cada actividad.

En cuanto a sus preguntas sobre:

1- Pérdida de memoria:

No, a menos que ponga una @Singletonanotación a los presentadores que proporcione. Dagger solo creará el objeto siempre que lo hagas @Injecten la clase de destino`. No creará otros presentadores en su escenario. Puede intentar usar Log para ver si se crean o no.

@Module
public class AppPresenterModule {

@Provides
@Singleton // <-- this will persists throughout the application, too many of these is not good
Activity1Presenter provideActivity1Presentor(Context context, ...some other params){
    Log.d("Activity1Presenter", "Activity1Presenter initiated");
    return new Activity1PresenterImpl(context, ...some other params);
}

@Provides // Activity2Presenter will be provided every time you @Inject into the activity
Activity2Presenter provideActivity2Presentor(Context context, ...some other params){
    Log.d("Activity2Presenter", "Activity2Presenter initiated");
    return new Activity2PresenterImpl(context, ...some other params);
}

.... Same with 48 others presenters.

}

2- Inyectas dos veces y registras su código hash

//MainActivity.java
@Inject Activity1Presenter mPresentation1
@Inject Activity1Presenter mPresentation2

@Inject Activity2Presenter mPresentation3
@Inject Activity2Presenter mPresentation4
//log will show Presentation2 being initiated twice

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    injectThisActivity();
    Log.d("Activity1Presenter1", mPresentation1.hashCode());
    Log.d("Activity1Presenter2", mPresentation2.hashCode());
    //it will shows that both have same hash, it's a Singleton
    Log.d("Activity2Presenter1", mPresentation3.hashCode());
    Log.d("Activity2Presenter2", mPresentation4.hashCode());
    //it will shows that both have different hash, hence different objects

3. No, los objetos solo se crearán cuando esté @Injecten las actividades, en lugar del inicio de la aplicación.

Liew Jun Tung
fuente
1
Gracias por el comentario, lo que ha dicho no está mal, pero creo que no es la mejor respuesta, consulte mi publicación de edición. Por lo tanto, no se pudo marcar como aceptado.
Mr Mike
@EpicPandaForce: Eh, pero tienes que instanciarlo en alguna parte. Algo tendrá que violar el principio de inversión de dependencia.
David Liu