Injeção de dependência no Kotlin com Koin

Seringa flutuando sobre uma mão

No artigo que criamos um Singleton do Database do Room, vimos que temos a necessidade de reutilizar o mesmo objeto devido ao LiveData. Porém, ao mesmo tempo, a implementação não é trivial.

Pensando nessa necessidade e outras, é muito comum a comunidade utilizar soluções prontas, como é o caso das famosas ferramentas de injeção de dependência.

Dentre as possibilidades, uma das mais famosas é o Dagger da Google que é bem poderoso e comum no ambiente Android. Porém, a compreensão e configuração exige um bom tempo do desenvolvedor, fazendo com que muitos até mesmo evitem o seu uso.

Pensando nisso, neste artigo vamos conhecer o Koin, uma ferramenta de injeção de dependência desenvolvida em Kotlin como alternativa ao Dagger! Interessado? Então bora começar:

Michael Jackson comendo pipoca

Separei esta seção para tirar algumas dúvidas iniciais sobre injeção de dependência, caso se sentir à vontade com o assunto, siga para o tópico Projeto utilizado no artigo

O que é dependência?

Dependências são objetos que uma classe precisa para realizar os comportamentos esperados, portanto, se uma classe acessa o banco de dados e usa um DAO pra isso, o DAO é uma dependência da classe.

O que é injeção de dependência?

Injeção de dependência é a técnica que delega a responsabilidade de inicializar dependências para o software. Por exemplo, ao invés instanciarmos as dependências em algum momento do código, o próprio framework de injeção de dependência realiza esses passos pra gente.

Por que preciso desta técnica?

O grande benefício é delegar a responsabilidade de inicialização das dependências, permitindo que membros do projeto apenas peçam o que precisam e a instância é fornecida automaticamente de acordo com o escopo necessário, como por exemplo, um Singleton ou Factory (instância sempre nova).

Dessa forma, temos menos trabalho de configuração e focamos no que é necessário:

Escrever código que vai fazer diferença no nosso App!

Num primeiro momento parece algo mágico, porém é válido ressaltar que cada framework exige configuração e regras para que a injeção seja feita… Mas não se preocupe, veremos como podemos fazer isso com o Koin 😃

Indo mais além na teoria, a técnica de injeção de dependência reflete a ideia de inversão de controle, dado que o desenvolvedor não se responsabiliza mais pelas dependências. Para mais detalhe sobre o assunto recomendo a leitura.

Projeto utilizado no artigo

Como exemplo, vou dar continuidade no projeto Tech Store, que uso como exemplo nos artigos sobre Architecture Components. Caso não tenha o projeto e não tenha interesse na leitura sobre o Room, fique à vontade em baixar o projeto a partir do seu repositório:

Adicionando o Koin no projeto

O Koin é uma lib externa, portanto, precisamos adicioná-lo como uma dependência:

implementation 'org.koin:koin-android:1.0.1'

Ao sincronizar o projeto, temos acesso as classes do Koin.

Conhecendo os módulos

A base de configuração do Koin é por meio de seus módulos, que são as entidades que mantém as instruções de como as dependências devem ser inicializadas. Isso significa que a partir deles, ensinamos o Koin como ele deve injetar as dependências pra gente.

Se considerarmos a classe ProductListActivity, que tem um DAO e um adapter como dependência, teríamos que definir como essas instâncias são criadas no módulo.

Configurando o primeiro módulo

Para configurar é bem simples, basta apenas utilizar a função module(), que é uma Higher-Order Function, e definir a instância desejada a partir da expressão lambda:

val techStoreModule = module {

}

Com base na nossa Activity, a dependência mais simples seria o adapter que exige apenas a referência de um Context, sendo assim, no módulo, declaramos da seguinte maneira:

val techStoreModule = module {
factory { ProductsListAdapter(context = get()) }
}

Para definir a instância, adicionamos a função factory, que indica ao Koin que cada vez que injetar uma instância com a referência ProductListAdapter, ele vai criar uma instância nova.

Note que para enviar o Context utilizamos a função get() que é uma função que resolve dependências das instâncias que definimos.

Nesse caso, o adapter depende de uma referência de Context que é uma entidade do Android Framework que não temos controle, portanto, enviamos o get() para que o Koin injete o Context pra gente.

Porém, é válido ressaltar que toda referência via get() tem que ser de conhecimento do Koin, ou seja, ser definida via módulo para que ele consiga criar a instância esperada.

No caso do Context não declaramos em nenhum momento como vai ser criado, certo? Porém, a referência de Context é um caso excepcional, pois ele sempre vai ser enviado durante a inicialização do Koin, veremos a seguir.

Inicializando o Koin

Ao definir um módulo, somos capazes de injetar as instâncias que foram definidas dentro dele, porém, é necessário inicializar o Koin a partir da função startKoin() dentro de uma entidade que tenha referência ao Context do Android. Podemos fazer essa inicialização no onCreate() da nossa Activity:

class ProductsListActivity : AppCompatActivity() {

private lateinit var productDao: ProductDao
private lateinit var adapter: ProductsListAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startKoin(this, listOf(techStoreModule))
// rest of the code
}
    // rest of the code
}

Como eu havíamos comentado, ao inicializar o Koin, somos obrigados a enviar a referência de Context que é esperada via primeiro argumento, logo em seguida, enviamos a lista de módulos que o Koin vai injetar pra gente, nesse caso, estamos enviando apenas o nosso módulo.

Injetando dependências

Com o Koin inicializado, podemos injetar as dependências como uma delegated property (que falo um pouco no artigo sobre inicialização via lazy) utilizando a keyword inject():

private val adapter: ProductsListAdapter by inject()

Assim como na inicialização via lazy, ao utilizar delegated properties, ganhamos a vantagem de manter a property imutável.

Consequentemente, temos um erro de compilação no trecho que faz a inicialização manualmente, nos obrigando a removê-la e manter a abordagem de injeção. Particularmente acho uma solução genial! 😃

Apenas com esse código, podemos testar o App que o adapter vai se injetado sem nenhum problema!

“Beleza Alex, mas como que fica a injeção do DAO?”

Considerando a pequena base que temos do Koin, vamos aumentar um pouco a complexidade e configurar a instância do DAO.

Definindo instâncias de objetos com mais complexidade

Para injetar um ProductDao não é tão simples assim, pois, considerando a regra na qual ele é instanciado, precisamos de uma referência do AppDatabase.

Assim sendo, o primeiro passo é definir como um AppDatabase pode ser instanciado dentro do módulo:

val techStoreModule = module {
factory { ProductsListAdapter(context = get()) }
single { Room.databaseBuilder(
get(),
AppDatabase::class.java,
"techstore-database")
.build()}
}

Veja que agora temos uma referência à instância do AppDatabase, porém, ao invés do factory, utilizamos o single, pois como vimos, o Database precisa manter uma instância única para que o LiveData funcione da maneira esperada e partir do single temos essa possibilidade.

Em seguida, basta apenas definir como esperamos a instância do DAO, essa instância pode seguir a mesma estratégia do Database, pois o DAO pode ser reutilizado sem nenhum problema.

val techStoreModule = module {
factory { ProductsListAdapter(context = get()) }
single { Room.databaseBuilder(
get(),
AppDatabase::class.java,
"techstore-database")
.build()}
single { get<AppDatabase>().productDao() }
}

Note que nesse momento temos uma abordagem diferente, pois como eu havia comentado, o Koin precisa saber instanciar uma dependência de outra dependência.

Nesse caso, pedimos ao Koin para que ele nos forneça uma instância do AppDatabase e que a partir dela, queremos que ele crie pra gente a instância do DAO. Agora podemos injetar também o nosso DAO:

private val productDao: ProductDao by inject()

Em seguida, removemos as inicializações manuais e podemos rodar o App que teremos uma instância do Database.

Porém, tem um detalhe importante! A instância fornecida para ProductsListActivity não é a mesma contida na FormProductActivity, ou seja, não teremos o comportamento esperado ao salvar um novo produto…

Para lidar com esse tipo de situação é bem simples, basta apenas injetar o DAO dentro da FormProductActivity.

Detalhes sobre a inicialização do Koin

Para que o Koin funcione da maneira esperada, a sua inicialização precisa ser feita apenas uma única vez em um local mais seguro do que uma Activity, pois se a Activity for destruída e criada novamente (algo bem comum de acontecer), perdemos as referências pelas quais ele fez a injeção.

Caso tenha curiosidade para testar o problema, apenas rotacione a tela do Android e veja que vai dar crash no App.

Sendo assim, um local comum de mantermos essa configuração é dentro da entidade mais estável de um App enquanto está rodando, a Application. Portanto, no onCreate() da Application, vamos inicializar o Koin:

class TechStoreApplication : Application() {

override fun onCreate() {
super.onCreate()
startKoin(this, listOf(techStoreModule))
}

}

Pronto, podemos rodar e testar o nosso App que ele vai funcionar de maneira esperada!

Batman e Robin balançando a cabeça expressando o “yeah”

Organização dos módulos

Perceba que a inicialização do Koin é feita a partir de uma lista de módulos, isso significa que criar vários módulos com diferentes finalidades é uma abordagem esperada para poder flexibilizar o que faz sentido ou não ser injetado. Com base no nosso contexto, podemos separar os nossos módulos em dois:

  • Banco de dados;
  • Interface do usuário.
val dbModule = module {
single { Room.databaseBuilder(
get(),
AppDatabase::class.java,
"techstore-database")
.build()}
single { get<AppDatabase>().productDao() }
}

val uiModule = module {
factory { ProductsListAdapter(context = get()) }
}

Então enviamos ambos os módulos durante a inicialização:

startKoin(this, listOf(dbModule, uiModule))

Utilizando a injeção de dependência nos testes

No final do artigo que envolveu o Singleton do AppDatabase, fizemos um teste de unidade para comprovar a unicidade da instância. Portanto, vamos adaptar o mesmo teste para que ele verifique se o Koin mantém a sua palavra.

Para aplicar testes no Koin, precisamos realizar algumas configurações. A primeira delas é adicionar a dependência que permite utilizá-lo em testes:

testImplementation 'org.koin:koin-test:1.0.1'

Em seguida, basta apenas herdar da classe KoinTest para que consigamos inicializar o Koin:

class DatabaseTest : KoinTest {

private val dbInstace: AppDatabase by inject()

@Test
fun `should get the same instance of Database when run threads simultaneously`() {
startKoin(listOf(dbModule))
repeat(10) {
thread(start = true) {
println(dbInstace)
}
}
Thread.sleep(500)
}

}

Com apenas essa configuração poderíamos rodar o teste sem nenhum problema, porém, a instância do AppDatabase depende de uma referência de Context.

Em testes de unidade, que não mantém a integração com o Android Framework, não é possível ter acesso a um Context válido, logo, precisamos mockar da mesma maneira como a implementação inicial.

Para mockar objetos em testes de unidade com o Koin, podemos utilizar a seguinte instrução:

startKoin(listOf(dbModule)) with (mock(Context::class.java))

Pronto! Ao rodar o teste tudo funciona como o esperado! E podemos até mesmo apagar aquela solução de Singleton manual hehe

Para saber mais

Além do que foi visto neste artigo, existe mais novidades no projeto Koin, como por exemplo, uma dependência destinada ao Archtecture Components. Caso tenha interesse em utilizar esse framework pra valer, recomendo a leitura da documentação. 😉

Código fonte

Se tiver dúvida ou quiser consultar o código fonte, fique à vontade em acessar o repositório no GitHub:

Conclusão

Neste artigo aprendemos como podemos configurar uma solução para injeção de dependência no Android utilizando o Koin.

Também aprendemos o básico dos conceitos sobre injeção de dependência e suas vantagens. Por fim, utilizamos o Koin para injetar as dependências das nossas Activities com base no escopo esperado.

Gostou do artigo? Aproveite e deixe o seu like para aumentar a divulgação, e também, comente sobre o que achou do Koin!


Quer aprender mais sobre Kotlin tanto no mundo mobile como no back-end? Então confira este agregador de conteúdo onde listo todos os conteúdos que escrevi de Kotlin e os que serão publicados mais pra frente 😉