android - implement - Daga: ¿deberíamos crear cada componente y módulo para cada Actividad/Fragmento?
dagger android support (4)
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 here .
Si examina el código fuente allí, puede ver que hay un solo componente con ámbito de aplicación (con un ciclo de vida de la duración de toda la aplicación) y luego componentes separados con ámbito de actividad para la actividad y el fragmento correspondiente a una funcionalidad dada en un proyecto. Por ejemplo, hay los siguientes paquetes:
addedittask
taskdetail
tasks
Dentro de cada paquete hay un módulo, componente, presentador, etc. Por ejemplo, dentro del
taskdetail
hay las siguientes clases:
TaskDetailActivity.java
TaskDetailComponent.java
TaskDetailContract.java
TaskDetailFragment.java
TaskDetailPresenter.java
TaskDetailPresenterModule.java
La ventaja de organizar 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. Efectivo de Java. En otras palabras, las clases agrupadas funcionalmente estarán en el mismo paquete y puede aprovechar
package-private
modificadores de accesibilidad
protected
y
package-private
para evitar usos no deseados de sus clases.
He estado trabajando con dagger2 por un tiempo. Y me confundí entre crear un componente / módulo propio para cada Actividad / Fragmento. Por favor, ayúdame a aclarar esto:
Por ejemplo, tenemos una aplicación, y la aplicación tiene aproximadamente 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, generalmente deberíamos organizar el código así:
-
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, ....) }
-
Crear ActivityScope:
@Scope @Documented @Retention(value=RUNTIME) public @interface ActivityScope { }
-
Crear componente y módulo para cada actividad. Por lo general, los pondré como clases estáticas dentro de la clase Actividad:
@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 dio otra implementación:
-
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. }
-
Crear 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 del todo buena, pero corríjame si me equivoco. Aquí están las razones:
-
Mucha pérdida de memoria :
- La aplicación creará 50 presentadores incluso si el usuario solo tiene 2 actividades abiertas.
- Después de que el usuario cierre una actividad, su presentador seguirá siendo
-
¿Qué sucede si quiero crear dos instancias de una actividad? (¿Cómo puede crear dos presentadores?)
-
La aplicación tardará mucho tiempo 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 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.
- Lo que mi amigo ha hecho no causa pérdidas de memoria a menos que ponga un Alcance a los métodos Provides. (Por ejemplo, @Singleton o @UserScope, ...)
- Podemos crear muchos presentadores, si el método Provides no tiene ningún Alcance. (Entonces, mi segundo punto también está mal)
- Dagger creará los presentadores solo cuando sean necesarios. (Entonces, la aplicación no tardará mucho en inicializarse, me confundió Lazy Injection)
Entonces, todas las razones que he dicho anteriormente son en su mayoría incorrectas. Pero no significa que debamos seguir mi idea de amigo, por dos razones:
-
No es bueno para la arquitectura de la fuente, cuando inicia a todos los presentadores en el módulo / componente. (Viola el principio de segregación de interfaz , quizás también el principio de responsabilidad única).
-
Cuando creamos un componente de alcance, sabremos cuándo se creó y cuándo se destruyó, lo cual es un gran beneficio para evitar pérdidas de memoria. Entonces, para cada Actividad debemos crear un Componente con un @ActivityScope. Imaginemos, con la implementación de mis amigos, que olvidamos poner algo de alcance en el método del proveedor => ocurrirán pérdidas de memoria.
En mi opinión, con una aplicación pequeña (solo unas pocas pantallas sin muchas dependencias o con dependencias similares), podríamos aplicar la idea de mis amigos, pero por supuesto no es recomendable.
Prefiere 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 destruyen los objetos, puede llamar a los del método juntos y el GC se ejecutará de inmediato:
System.runFinalization();
System.gc();
Si usa solo uno de estos métodos, GC se ejecutará más tarde y puede obtener resultados incorrectos.
Declarar un módulo separado para cada
Activity
no es una buena idea en absoluto.
Declarar un componente separado para cada
Activity
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 relacionado con el ciclo de vida de la aplicación y usarlo para inyectarlo en todas las
Activities
tampoco es la solución óptima (este es el enfoque de su amigo).
No es óptimo porque:
-
Te restringe a un solo alcance (
@Singleton
o uno personalizado) - El único alcance al que está restringido hace que los objetos inyectados sean "aplicaciones únicas", por lo tanto, los errores en el alcance o el uso incorrecto de los objetos con alcance pueden causar fácilmente pérdidas de memoria global.
-
También querrá usar Dagger2 para inyectar en los
Services
, pero losServices
pueden requerir diferentes objetos que lasActivities
(por ejemplo, losServices
no necesitan presentadores, no tienenFragmentManager
, etc.). Al usar un solo componente, pierde la flexibilidad de definir diferentes gráficos de objetos para diferentes componentes.
Por lo tanto, un componente por
Activity
es una exageración, pero un componente único para toda la aplicación no es lo suficientemente flexible.
La solución óptima está entre estos extremos (como suele ser).
Yo uso el siguiente enfoque:
-
Componente único de "aplicación" que proporciona objetos "globales" (por ejemplo, objetos que contienen un estado global que se comparte entre todos los componentes de la aplicación).
Instanciado en la
Application
. -
Subcomponente "Controlador" del componente "aplicación" que proporciona los objetos requeridos por todos los "controladores" orientados al usuario (en mi arquitectura, estas son
Activities
yFragments
). Instanciado en cadaActivity
yFragment
. -
Subcomponente "Servicio" del componente "aplicación" que proporciona objetos requeridos por todos los
Services
. Instanciado en cadaService
.
El siguiente es 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 2018
Publiqué un curso completo sobre inyección de dependencia 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 se ajustan al esquema general de inyección de dependencia.
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.
Tal 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", uniéndolos así. Esto va en contra del objetivo principal de la inyección de dependencia, que es mantener disjuntos los conjuntos de clases "Construcción" y "Funcional".
Alcance de la 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 inyección de dependencia:
La primera opción crea un componente de sub-ámbito para cada actividad, donde la actividad puede crear componentes de sub-ámbito que solo proporcionan la dependencia (presentador) para esa actividad en particular.
La segunda opción crea un único componente
@Singleton
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 característica, sino por capa.
He usado ambos, ambos funcionan y ambos tienen sentido.
La única desventaja de la primera solución (si está utilizando
@Component(dependencies={...}
lugar de
@Subcomponent
) es que debe asegurarse de que no es la actividad la que crea su propio módulo internamente, porque entonces no puede reemplace las implementaciones del método del módulo con simulacros. De nuevo, 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 simulacros directamente.
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 las instancias de clase en los Módulos en lugar de instanciarlas en el método onCreate de Actividades.
Normalmente 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.
}
}
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 como una derrota con el propósito de la daga no? Prefiero crear instancias 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 anotación
@Singleton
a los presentadores que proporcione.
Dagger solo creará el objeto cada vez que hagas un
@Inject
en la clase objetivo`.
No creará los otros presentadores en su escenario.
Puede intentar usar Log para ver si se crearon 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- Inyecta dos veces y registra 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
@Inject
a las actividades, en lugar del inicio de la aplicación.