Adaga- Devemos criar cada componente e módulo para cada atividade / fragmento

85

Estou trabalhando com o dagger2 há algum tempo. E fiquei confuso se deveria criar um componente / módulo próprio para cada atividade / fragmento. Por favor me ajude a esclarecer isso:

Por exemplo, temos um aplicativo, e o aplicativo tem cerca de 50 telas. Implementaremos o código seguindo o padrão MVP e Dagger2 para DI. Suponha que temos 50 atividades e 50 apresentadores.

Na minha opinião, normalmente devemos organizar o código assim:

  1. Crie um AppComponent e AppModule que fornecerá todos os objetos que serão usados ​​enquanto o aplicativo estiver aberto.

    @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. Criar ActivityScope:

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Crie um componente e um módulo para cada atividade. Normalmente, vou colocá-los como classes estáticas dentro da classe 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.
    

Esses são apenas exemplos muito simples para mostrar como eu implementaria isso.

Mas um amigo meu acabou de me dar outra implementação:

  1. Crie PresenterModule que fornecerá a todos os apresentadores:

    @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. Crie AppModule e 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, ....)
    
    }
    

Sua explicação é: ele não precisa criar componentes e módulos para cada atividade. Acho que a ideia dos meus amigos não é nada boa, mas corrija-me se eu estiver errado. Aqui estão as razões:

  1. Muitos vazamentos de memória :

    • O aplicativo criará 50 apresentadores, mesmo se o usuário tiver apenas 2 atividades abertas.
    • Depois que o usuário fecha uma atividade, seu apresentador ainda permanecerá
  2. O que acontece se eu quiser criar duas instâncias de uma atividade? (como ele pode criar dois apresentadores)

  3. Levará muito tempo para o aplicativo inicializar (porque ele precisa criar muitos apresentadores, objetos ...)

Desculpe pelo longo post, mas por favor me ajude a esclarecer isso para mim e para meu amigo, não consigo convencê-lo. Seus comentários serão muito apreciados.

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

Edite depois de fazer uma demonstração.

Primeiramente, obrigado pela resposta @pandawarrior. Eu deveria ter criado uma demonstração antes de fazer esta pergunta. Espero que minha conclusão aqui possa ajudar outra pessoa.

  1. O que meu amigo fez não causa vazamentos de memória, a menos que ele coloque qualquer Scope nos métodos Provides. (Por exemplo @Singleton ou @UserScope, ...)
  2. Podemos criar muitos apresentadores, se o método Provides não tiver nenhum Scope. (Então, meu segundo ponto também está errado)
  3. Dagger criará os apresentadores somente quando eles forem necessários. (Portanto, o aplicativo não demorará muito para inicializar, fiquei confuso com a injeção lenta)

Portanto, todas as razões que disse acima estão erradas. Mas isso não significa que devemos seguir a ideia do meu amigo, por dois motivos:

  1. Não é bom para a arquitetura do código-fonte, quando ele inicia todos os apresentadores no módulo / componente. (Isso viola o princípio de segregação de interface , talvez o princípio de responsabilidade única também).

  2. Quando criamos um componente de escopo, saberemos quando ele é criado e quando é destruído, o que é um grande benefício para evitar vazamentos de memória. Portanto, para cada Activity devemos criar um Component com um @ActivityScope. Vamos imaginar, com a implementação de meus amigos, que esquecemos de colocar algum escopo no método Provider => vazamentos de memória irão ocorrer.

Na minha opinião, com um aplicativo pequeno (apenas algumas telas sem muitas dependências ou com dependências semelhantes), poderíamos aplicar a ideia do meu amigo, mas é claro que não é recomendável.

Prefira ler mais sobre: O que determina o ciclo de vida de um componente (gráfico de objeto) no Dagger 2? Escopo da atividade Dagger2, quantos módulos / componentes eu preciso?

E mais uma observação: se você quiser ver quando os objetos são destruídos, você pode chamar aqueles do método juntos e o GC será executado imediatamente:

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

Se você usar apenas um desses métodos, o GC será executado mais tarde e você poderá obter resultados errados.

Sr. Mike
fonte

Respostas:

85

Declarar um módulo separado para cada Activityum não é uma boa ideia. Declarar componentes separados para cada um Activityé ainda pior. O raciocínio por trás disso é muito simples - você realmente não precisa de todos esses módulos / componentes (como você já viu por si mesmo).

No entanto, ter apenas um componente vinculado ao Applicationciclo de vida de e usá-lo para injeção em todos Activitiestambém não é a solução ideal (esta é a abordagem do seu amigo). Não é ideal porque:

  1. Ele restringe você a apenas um escopo ( @Singletonou um personalizado)
  2. O único escopo ao qual você está restrito torna os objetos injetados "singletons de aplicativo", portanto, erros no escopo ou uso incorreto de objetos com escopo podem facilmente causar vazamentos de memória global
  3. Você vai querer usar Dagger2 para fazer a injeção Servicestambém, mas Servicespode exigir objetos diferentes Activities(por exemplo Services, não precisa de apresentadores, não tem FragmentManager, etc.). Ao usar um único componente, você perde a flexibilidade de definir diferentes gráficos de objetos para diferentes componentes.

Portanto, um componente por Activityé um exagero, mas um único componente para todo o aplicativo não é flexível o suficiente. A solução ideal está entre esses extremos (como geralmente é).

Eu uso a seguinte abordagem:

  1. Componente único de "aplicativo" que fornece objetos "globais" (por exemplo, objetos que mantêm o estado global que é compartilhado entre todos os componentes do aplicativo). Instanciado em Application.
  2. O subcomponente "controlador" do componente "aplicativo" que fornece objetos que são exigidos por todos os "controladores" voltados para o usuário (na minha arquitetura são Activitiese Fragments). Instanciado em cada um Activitye Fragment.
  3. Subcomponente "Serviço" do componente "aplicativo" que fornece os objetos exigidos por todos Services. Instanciado em cada um Service.

A seguir está um exemplo de como você pode implementar a mesma abordagem.


Editar julho de 2017

Publiquei um tutorial em vídeo que mostra como estruturar o código de injeção de dependência do Dagger no aplicativo Android : Android Dagger for Professionals Tutorial .


Editar fevereiro de 2018

Publiquei um curso completo sobre injeção de dependência no Android .

Neste curso, eu explico a teoria da injeção de dependência e mostro como ela surge naturalmente no aplicativo Android. Em seguida, demonstro como as construções de Dagger se encaixam no esquema geral de injeção de dependência.

Se você fizer este curso, entenderá por que a ideia de ter uma definição separada de módulo / componente para cada atividade / fragmento é basicamente falha da maneira mais fundamental.

Tal abordagem faz com que a estrutura da camada de apresentação do conjunto "Funcional" de classes seja espelhada na estrutura do conjunto de classes "Construção", acoplando-as assim. Isso vai contra o objetivo principal da injeção de dependência, que é manter os conjuntos de classes "Construção" e "Funcional" separados.


Escopo do aplicativo:

@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);
    }
}

Escopo do 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)
}

E então em 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;
    }
}

Informações adicionais sobre injeção de dependência:

Dagger 2 Scopes desmistificados

Injeção de dependência no Android

Vasiliy
fonte
1
Obrigado @vasiliy por compartilhar sua opinião. É exatamente assim que eu usaria e atualmente a estratégia seguida. No caso de um padrão MVP, o referido ControllerModuleirá criar um novo Presentere então o apresentador é injetado no Activityou Fragment. Alguma opinião sólida a favor ou contra isso?
Wahib Ul Haq,
@Vasiliy, li seu artigo inteiro e descobri que talvez você não considerasse os interatores e apresentadores no mecanismo. O ControllerModule fornecerá todas as dependências de interatores e apresentadores ? Por favor, dê uma pequena dica caso eu tenha esquecido alguma coisa.
iamcrypticcoder
@mahbub.kuet, se eu entendi a que você se refere por "interatores" e "apresentadores", ControllerComponentdeveria injetá-los. Se você os conecta internamente ControllerModuleou introduz um módulo adicional, depende de você. Em aplicativos reais, aconselho o uso de vários módulos por abordagem de componente em vez de colocar tudo em um único módulo. Aqui está um exemplo de ApplicationComponent, mas o controlador será o mesmo: github.com/techyourchance/idocare-android/tree/master/app/src/…
Vasiliy
2
@ Mr.Hyde, em geral sim, mas então você terá que declarar explicitamente em ApplicationComponenttodas as dependências que ControllerComponentpode usar. Além disso, a contagem de métodos do código gerado será maior. Ainda não encontrei um bom motivo para usar componentes dependentes.
Vasiliy
1
Estou usando essa abordagem em todos os meus projetos hoje e explicitamente não uso nada do dagger.androidpacote porque acho que está mal motivado. Portanto, este exemplo ainda está muito atualizado e ainda é a melhor maneira de fazer DI no Android IMHO.
Vasiliy
15

Alguns dos melhores exemplos de como organizar seus componentes, módulos e pacotes podem ser encontrados no repositório Google Android Architecture Blueprints Github aqui .

Se você examinar o código-fonte lá, poderá ver que há um único componente com escopo de aplicativo (com um ciclo de vida da duração de todo o aplicativo) e, em seguida, componentes com escopo de atividade separados para a atividade e fragmento correspondente a uma determinada funcionalidade em um projeto. Por exemplo, existem os seguintes pacotes:

addedittask
taskdetail
tasks

Dentro de cada pacote existe um módulo, componente, apresentador etc. Por exemplo, dentro taskdetailexistem as seguintes classes:

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

A vantagem de organizar dessa forma (em vez de agrupar todas as atividades em um componente ou módulo) é que você pode aproveitar os modificadores de acessibilidade Java e cumprir o item 13. Efetivo de Java. Em outras palavras, as classes agrupadas funcionalmente serão as mesmas pacote e você pode tirar proveito de protectede package-private modificadores de acessibilidade para prevenir usos não intencionais de suas classes.

David Rawson
fonte
1
esta também é minha abordagem preferida. Não gosto de atividades / fragmentos que tenham acesso a coisas que não deveriam.
João Sousa
3

A primeira opção cria um componente de subescopo para cada atividade, em que a atividade é capaz de criar componentes de subescopo que fornecem apenas a dependência (apresentador) para aquela atividade específica.

A segunda opção cria um único @Singletoncomponente que é capaz de fornecer aos apresentadores como dependências sem escopo, ou seja, quando você os acessa, cria uma nova instância do apresentador a cada vez. (Não, ele não cria uma nova instância até que você solicite uma).


Tecnicamente, nenhuma das abordagens é pior que a outra. A primeira abordagem não separa os apresentadores por recurso, mas por camada.

Eu usei os dois, ambos funcionam e fazem sentido.

A única desvantagem da primeira solução (se você estiver usando em @Component(dependencies={...}vez de @Subcomponent) é que você precisa ter certeza de que não é a Activity que cria seu próprio módulo internamente, porque então você não pode substituir implementações de método de módulo por simulações. Então, novamente, se você usar injeção de construtor em vez de injeção de campo, você pode apenas criar a classe diretamente com o construtor, fornecendo diretamente mocks.

EpicPandaForce
fonte
1

Use em Provider<"your component's name">vez da implementação de componentes simples para evitar vazamentos de memória e criar toneladas de componentes inúteis. Portanto, seus componentes serão criados por preguiçoso quando você chamar o método get (), já que você não fornece uma instância do componente, mas apenas o provedor. Portanto, seu apresentador será aplicado se .get () do provedor for chamado. Leia sobre o Provedor aqui e aplique-o. ( Documentação oficial do punhal )


E outra ótima maneira é usar multibinding. De acordo com ele, você deve vincular seus apresentadores ao mapa e criá-los por meio de provedores quando necessário. ( aqui estão os documentos sobre multibinding )

Konstantin Levitskiy
fonte
-5

Seu amigo está correto, você realmente não precisa criar componentes e módulos para todas as atividades. Supõe-se que o Dagger ajuda a reduzir o código confuso e torna suas atividades Android mais limpas, delegando instanciações de classe aos Módulos em vez de instanciá-las no método onCreate de Activities.

Normalmente faremos assim

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.
}

}

Você faz isso ao invés

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);
}}

Então, escrever muitas coisas derrota o propósito da adaga, não? Prefiro instanciar meus apresentadores em Atividades se tiver que criar Módulos e Componentes para todas as Atividades.

Quanto às suas perguntas sobre:

1- Vazamento de memória:

Não, a menos que você coloque uma @Singletonanotação para os apresentadores que fornecer. Dagger só criará o objeto sempre que você fizer um @Injectna classe de destino. Isso não criará os outros apresentadores em seu cenário. Você pode tentar usar o Log para ver se eles foram criados ou não.

@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- Você injeta duas vezes e registra o 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. Não, os objetos só serão criados quando você entrar @Injectnas atividades, ao invés do app init.

Liew Jun Tung
fonte
1
Obrigado pelo comentário, o que você disse não está errado, mas acho que não é a melhor resposta, por favor, veja meu post de edição. Portanto, não foi possível marcá-lo como aceito.
Sr. Mike,
@EpicPandaForce: Eh, mas você tem que instanciá-lo em algum lugar. Algo terá que violar o princípio de inversão de dependência.
David Liu