O futuro da injeção de dependência no Android

Trago à sua atenção uma tradução do artigo original de Jamie Sanson
imagem


Criando atividade antes do Android 9 Pie


A injeção de dependência (DI) é um modelo comum usado em todas as formas de desenvolvimento por vários motivos. Graças ao projeto Dagger, ele é usado como modelo usado no desenvolvimento para Android. As mudanças recentes no Android 9 Pie nos fizeram ter agora mais opções no que diz respeito ao DI, especialmente com a nova classe AppComponentFactory .




DI é muito importante quando se trata de desenvolvimento moderno do Android. Isso permite reduzir a quantidade total de código ao obter links para serviços usados ​​entre classes e geralmente divide bem o aplicativo em componentes. Neste artigo, focaremos no Dagger 2, a biblioteca DI mais comum usada no desenvolvimento do Android. Supõe-se que você já tenha conhecimento básico de como isso funciona, mas não é necessário entender todas as sutilezas. Vale ressaltar que este artigo é um pouco de aventura. Isso é interessante e tudo, mas no momento em que foi escrito, o Android 9 Pie nem apareceu no painel da versão da plataforma , portanto esse tópico provavelmente não será relevante para o desenvolvimento cotidiano por pelo menos vários anos.


Injeção de dependência no Android hoje


Simplificando, usamos o DI para fornecer instâncias de classes de “dependência” para nossas classes dependentes, ou seja, aquelas que fazem o trabalho. Digamos que usamos o padrão de repositório para processar nossa lógica relacionada a dados e queremos usar nosso repositório em Activity para mostrar alguns dados para o usuário. Podemos querer usar o mesmo repositório em vários lugares, portanto, usamos injeção de dependência para facilitar o compartilhamento da mesma instância entre várias classes diferentes.


Primeiro, forneceremos um repositório. Definiremos a função Provides no módulo, permitindo que o Dagger saiba que esta é exatamente a instância que queremos implementar. Observe que nosso repositório precisa de uma instância de contexto para trabalhar com arquivos e a rede. Forneceremos o contexto do aplicativo.


 @Module class AppModule(val appContext: Context) { @Provides @ApplicationScope fun provideApplicationContext(): Context = appContext @Provides @ApplicationScope fun provideRepository(context: Context): Repository = Repository(context) } 

Agora precisamos definir o Component para lidar com a implementação das classes nas quais queremos usar nosso Repository .


 @ApplicationScope @Component(modules = [AppModule::class]) interface ApplicationComponent { fun inject(activity: MainActivity) } 

Finalmente, podemos configurar nossa Activity para usar nosso repositório. Suponha que tenhamos criado uma instância do nosso ApplicationComponent em outro lugar.


 class MainActivity: AppCompatActivity() { @Inject lateinit var repository: Repository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //    application.applicationComponent.inject(this) //       } } 

Isso é tudo! Acabamos de configurar a injeção de dependência no aplicativo usando o Dagger. Existem várias maneiras de fazer isso, mas essa parece ser a abordagem mais fácil.


O que há de errado com a abordagem atual?


Nos exemplos acima, vimos dois tipos diferentes de injeções, uma mais óbvia que a outra.


O primeiro que você pode ter perdido é conhecido como incorporação no construtor . Este é um método de fornecer dependências por meio do construtor de uma classe, o que significa que uma classe que usa dependências não tem idéia sobre a origem das instâncias. Essa é considerada a forma mais pura de injeção de dependência, pois encapsula nossa lógica de injeção em nossas classes de Module perfeitamente. Em nosso exemplo, usamos essa abordagem para fornecer um repositório:


 fun provideRepository(context: Context): Repository = Repository(context) 

Para isso, precisávamos de Context , que fornecemos na função provideApplicationContext() .


A segunda coisa mais óbvia que vimos é a implementação da classe no campo . Este método foi usado em nossa MainActivity para fornecer nossa loja. Aqui, definimos os campos como destinatários das injeções usando a anotação Inject . Em seguida, em nossa função onCreate informamos ao ApplicationComponent que as dependências devem ser injetadas em nossos campos. Não parece tão limpo quanto a incorporação em um construtor, porque temos uma referência explícita ao nosso componente, o que significa que o conceito de incorporação está se infiltrando em nossas classes dependentes. Outra falha nas classes do Android Framework, pois precisamos ter certeza de que a primeira coisa que fazemos é fornecer dependências. Se isso acontecer no ponto errado do ciclo de vida, podemos tentar acidentalmente usar um objeto que ainda não foi inicializado.


Idealmente, você deve se livrar completamente das implementações nos campos de classe. Essa abordagem ignora as informações de implementação de classes que não precisam saber sobre isso e pode causar problemas no ciclo de vida. Vimos tentativas de fazê-lo melhor, e o Dagger no Android é uma maneira bastante confiável, mas no final seria melhor se pudéssemos usar a incorporação no construtor. Atualmente, não podemos usar essa abordagem para várias classes de estrutura, como "Atividade", "Serviço", "Aplicativo" etc., pois elas são criadas para nós pelo sistema. Parece que, no momento, estamos presos à introdução de classes em campos. No entanto, o Android 9 Pie está preparando algo interessante, que, talvez, mudará fundamentalmente tudo.


Injeção de dependência no Android 9 Pie


Conforme mencionado no começo do artigo, o Android 9 Pie possui uma classe AppComponentFactory. A documentação para isso é bastante escassa e é simplesmente postada no site do desenvolvedor, como tal:


A interface usada para controlar a criação de elementos do manifesto.

Isso é intrigante. "Elementos manifestos" aqui se referem às classes que listamos em nosso arquivo AndroidManifest - como Activity, Service e nossa classe Application. Isso nos permite "controlar a criação" desses elementos ... então, ei, agora podemos definir as regras para a criação de nossas atividades? Que delícia!


Vamos cavar mais fundo. Começaremos estendendo o AppComponentFactory e substituindo o método instantiateActivity .


 class InjectionComponentFactory: AppComponentFactory() { private val repository = NonContextRepository() override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return when { className == MainActivity::class.java.name -> MainActivity(repository) else -> super.instantiateActivity(cl, className, intent) } } } 

Agora precisamos declarar nossa fábrica de componentes no manifesto dentro da tag do aplicativo .


 <application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name=".InjectionApp" android:appComponentFactory="com.mypackage.injectiontest.component.InjectionComponentFactory" android:theme="@style/AppTheme" tools:replace="android:appComponentFactory"> 

Finalmente, podemos lançar nosso aplicativo ... e funciona! Nosso NonContextRepository fornecido por meio do construtor MainActivity. Graciosamente!


Por favor, note que existem algumas reservas. Não podemos usar o Context aqui, pois mesmo antes de sua existência, ocorre uma chamada para nossa função - isso é confuso! Podemos ir além para que o construtor implemente nossa classe Application, mas vamos ver como o Dagger pode tornar isso ainda mais fácil.


Conheça - Dagger Multi-Binds


Não entrarei em detalhes da operação de ligação múltipla do Dagger sob o capô, pois isso está além do escopo deste artigo. Tudo que você precisa saber é que ele fornece uma boa maneira de injetar no construtor de classe sem precisar chamar manualmente o construtor. Podemos usar isso para implementar facilmente classes de estrutura de maneira escalável. Vamos ver como tudo isso se soma.


Vamos configurar nossa Atividade primeiro para descobrir para onde ir a seguir.


 class MainActivity @Inject constructor( private val repository: NonContextRepository ): Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //       } } 

Isso mostra imediatamente que quase não há menção à injeção de dependência. A única coisa que vemos é a anotação Inject antes do construtor.


Agora você precisa alterar o componente e o módulo Dagger:


 @Component(modules = [ApplicationModule::class]) interface ApplicationComponent { fun inject(factory: InjectionComponentFactory) } 

 @Module(includes = [ComponentModule::class]) class ApplicationModule { @Provides fun provideRepository(): NonContextRepository = NonContextRepository() } 

Nada mudou muito. Agora, precisamos apenas implementar nossa fábrica de componentes, mas como criamos nossos elementos manifestos? Aqui precisamos de um ComponentModule . Vamos ver:


 @Module abstract class ComponentModule { @Binds @IntoMap @ComponentKey(MainActivity::class) abstract fun bindMainActivity(activity: MainActivity): Any @Binds abstract fun bindComponentHelper(componentHelper: ComponentHelper): ComponentInstanceHelper } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey internal annotation class ComponentKey(val clazz: KClass<out Any>) 

Sim, bem, apenas algumas anotações. Aqui, conectamos nossa Activity a um mapa, implementamos esse mapa em nossa classe ComponentHelper e fornecemos esse ComponentHelper - tudo em duas instruções Binds . O Dagger sabe como instanciar nossa MainActivity graças à anotação MainActivity Inject para que ele possa "vincular" o provedor a essa classe, fornecendo automaticamente as dependências que precisamos para o construtor. Nosso ComponentHelper o seguinte.


 class ComponentHelper @Inject constructor( private val creators: Map<Class<out Any>, @JvmSuppressWildcards Provider<Any>> ): ComponentInstanceHelper { @Suppress("UNCHECKED_CAST") override fun <T> resolve(className: String): T? = creators .filter { it.key.name == className } .values .firstOrNull() ?.get() as? T } interface InstanceComponentHelper { fun <T> resolve(className: String): T? } 

Simplificando, agora temos um mapa de classes para fornecedores dessas classes. Quando tentamos resolver uma classe pelo nome, simplesmente encontramos o provedor dessa classe (se tivermos uma), chamamos para obter uma nova instância dessa classe e a retornamos.


Finalmente, precisamos fazer alterações em nosso AppComponentFactory para usar nossa nova classe auxiliar.


 class InjectionComponentFactory: AppComponentFactory() { @Inject lateinit var componentHelper: ComponentInstanceHelper init { DaggerApplicationComponent.create().inject(this) } override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return componentHelper .resolve<Activity>(className) ?.apply { setIntent(intent) } ?: super.instantiateActivity(cl, className, intent) } } 

Execute o código novamente. Tudo funciona! Que delícia.


Problemas de implementação do construtor


Esse título pode não parecer muito impressionante. Embora possamos incorporar a maioria das instâncias no modo normal injetando-as no construtor, não temos uma maneira óbvia de fornecer contexto para nossas dependências de maneira padrão. Mas o Context no Android é tudo. É necessário para acessar configurações, rede, configuração de aplicativos e muito mais. Nossas dependências geralmente são coisas que usam serviços relacionados a dados, como rede e configurações. Podemos contornar isso reescrevendo nossas dependências para serem funções puras ou inicializando tudo com instâncias de contexto em nossa classe Application , mas é preciso muito mais trabalho para determinar a melhor maneira de fazer isso.


Outra desvantagem dessa abordagem é a definição de escopo. No Dagger, um dos principais conceitos para implementar a injeção de dependência de alto desempenho com uma boa separação de relacionamentos de classe é a modularidade do gráfico de objetos e o uso do escopo. Embora essa abordagem não proíba o uso de módulos, ela limita o uso do escopo. AppComponentFactory existe em um nível completamente diferente de abstração em relação às nossas classes de estrutura padrão - não podemos obter um link para ele programaticamente, portanto, não temos como instruí-lo a fornecer dependências para o Activity em um escopo diferente.


Existem muitas maneiras de resolver nossos problemas com escopos na prática, uma das quais é usar o FragmentFactory para incorporar nossos fragmentos no construtor com escopos. Não entrarei em detalhes, mas agora temos um método para controlar a criação de fragmentos, que não apenas nos dá uma liberdade muito maior em termos de escopo, mas também tem compatibilidade com versões anteriores.


Conclusão


O Android 9 Pie introduziu uma maneira de usar a incorporação no construtor para fornecer dependências em nossas classes de estrutura, como "Atividade" e "Aplicativo". Vimos que, com a Dagger Multi-binding, podemos facilmente fornecer dependências no nível do aplicativo.


Um construtor que implementa todos os nossos componentes é extremamente atraente, e podemos até fazer algo para fazê-lo funcionar corretamente com instâncias de contexto. Este é um futuro promissor, mas está disponível apenas a partir da API 28. Se você deseja atingir menos de 0,5% dos usuários, pode experimentá-lo. Caso contrário, você deve esperar e ver se esse método permanece relevante em alguns anos.

Source: https://habr.com/ru/post/pt444530/


All Articles