Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

Olá pessoal! No último post integramos a métrica de cobertura de código ao nosso projeto. Hoje, vamos começar a mexer um pouco mais com o código da nossa aplicação, começando pela integração de uma biblioteca para nos fornecer um mecanismo de injeção de dependências (ou Dependency Injection – DI).

Quando falamos de DI no Android, logo nos vem a ideia de utilizar o Dagger para tal tarefa. Apesar de ser uma ferramenta muito poderosa, com estabilidade comprovada em produção, ela é um pouco verbosa e com uma quantidade relativamente grande de boilerplate para configurarmos – principalmente quando levamos em consideração um projeto 100% Kotlin como é o nosso caso.

Pensando, então, em um contexto puramente Kotlin, temos duas bibliotecas em evidência atualmente na comunidade, a Kodein e a Koin. A Kodein é uma solução mais robusta, focada em multi-plataforma (pensando nos diversos targets do Kotlin, como JVM, nativo e JavaScript), com uma recente refatoração na versão 5. Já a Koin tem uma abordagem mais minimalista e, ao meu ver, um ponto bem positivo: a integração com os Architecture Components (AC) do Android.

Sem pensar em otimizações prematuras, e imaginando a evolução do projeto, vamos fazer um setup inicial desse nosso mecanismo de fornecimento de dependências, com uma integração inicial com os AC. Eles vão nos ajudar a manter nossas Activities e Fragments mais limpos, isolar lógicas e facilitar testes.

O primeiro passo aqui, será adicionar as dependências da Koin e dos Architecture Components (no nosso caso, ViewModel e LiveData). Vamos adicionar as versões no arquivo build.gradle e as dependências no app/build.gradle.

// build.gradle
buildscript {
  ext.versions = [
    'kotlin': '1.2.41',
    'supportLibrary': '27.1.1',
    'jacoco': '0.8.1',
    'archComponents': '1.1.1',
    'koin': '0.9.3',
  ]
...
}
// app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'org.jetbrains.kotlin.kapt'

...

dependencies {
  ...

  implementation "android.arch.lifecycle:extensions:$versions.archComponents"
  kapt "android.arch.lifecycle:compiler:$versions.archComponents"

  implementation "org.koin:koin-android:$versions.koin"
  implementation "org.koin:koin-android-architecture:$versions.koin"
}

Como os AC necessitam de uma dependência de processador de anotações (annotationProcessor), precisamos aplicar o plugin do kapt (o processador de anotações do Kotlin), e adicionar a dependência do compiler com o escopo kapt.

Primeiramente vamos criar um ViewModel de exemplo, quase um placeholder. Como ainda não temos nenhuma feature no nosso app, faremos isso para validar em parte nossa arquitetura com o Koin. Sendo assim, criei um MainViewModel que será utilizado pela nossa MainActivity (que até então está vazia).

package net.rafaeltoledo.social

import android.arch.lifecycle.ViewModel

class MainViewModel(private val string: String) : ViewModel() {

  fun getString() = string
}

Na nossa Activity, vamos pedir uma instância desse ViewModel através do Koin e (por enquanto), simplesmente exibir o conteúdo do método getString() em um TextView.

package net.rafaeltoledo.social 

import android.os.Bundle 
import android.support.v7.app.AppCompatActivity 
import android.widget.TextView 

import org.koin.android.architecture.ext.viewModel 

class MainActivity : AppCompatActivity() { 
  private val mainViewModel: MainViewModel by viewModel() 
  
  override fun onCreate(savedInstanceState: Bundle?) { 
    super.onCreate(savedInstanceState) 
    setContentView(TextView(this).apply { 
      id = R.id.content 
      text = mainViewModel.getString() 
    }) 
  } 
}

Para que essa “mágica” aconteça – e que o Koin seja capaz de nos fornecer uma instância de qualquer ViewModel -, precisamos configurar os módulos, e ensinar a biblioteca a construir esses objetos.

Primeiramente, perceba que o nosso ViewModel recebe uma String como parâmetro de seu construtor. Para que o Koin nos forneça um objeto do ViewModel, precisamos de alguma forma fornecer esse valor. Para isso, criarei um pacote chamado di, contendo dois arquivos: FirstModule (que fornecerá essa String) e ViewModelModule (que fornecerá a instância do ViewModel).

Estrutura de pacotes

Para criar os módulos, faremos uso da função applicationContext do Koin:

// FirstModule.kt 
package net.rafaeltoledo.social.di 

import org.koin.dsl.module.applicationContext 

val firstModule = applicationContext { 
  bean { "Social App" } 
}
// ViewModelModule.kt 
package net.rafaeltoledo.social.di 

import net.rafaeltoledo.social.MainViewModel 

import org.koin.android.architecture.ext.viewModel 
import org.koin.dsl.module.applicationContext 

val viewModelModule = applicationContext { 
  viewModel { MainViewModel(get()) } 
}

Para o valor do tipo String, utilizamos bean, enquanto que, para o ViewModel, utilizamos viewModel. Para fornecer os parâmetros necessários a criação dos objetos (no caso aqui, do ViewModel), basta passarmos get() como parâmetro.

Por final, para amarrarmos todas as pontas, basta configurarmos o Koin na classe Application. Como ainda não temos uma implementação própria, vamos criar uma classe SocialApp para que possamos fazer essa inicialização.

package net.rafaeltoledo.social 

import android.app.Application 

import net.rafaeltoledo.social.di.firstModule 
import net.rafaeltoledo.social.di.viewModelModule 

import org.koin.android.ext.android.startKoin 

class SocialApp : Application() { 
  
  override fun onCreate() { 
    super.onCreate() 
    startKoin(listOf( 
      viewModelModule, 
      firstModule 
    )) 
  } 
}

A inicialização do Koin se dá por meio da extension function startKoin(), para a qual passamos a lista de módulos – no nosso caso, viewModelModule e firstModule. É importante lembrar de adicionar a nossa implementação da Application ao AndroidManifest.

O resultado disso é a nossa string exibida corretamente na MainActivity.

Resultado Final

Tudo pronto? Ainda não! Cadê os testes?

Todo esse overhead de configuração é inútil se não estamos utilizando com algum propósito. Além de deixar nossas classes mais enxutas, a ideia é facilitar a troca de objetos por mocks ou mesmo por valores controlados para a execução dos testes.

Apesar de não ser o ideal, por enquanto vamos continuar com o Robolectric. Como nosso setup do Koin está localizado na Application, precisamos substituir ou alterar a sua implementação para que possamos preparar a execução dos testes. Nesse aspecto, o Robolectric possui uma facilidade muito bacana: basta criar uma classe com o mesmo nome da sua implementação de Application, utilizando o prefixo Test. Com isso, ele automaticamente utilizará a versão de testes da sua Application durante a execução.

Sendo assim, dentro do nosso source set de testes, criarei um arquivo chamado TestSetup.kt com o seguinte conteúdo:

// TestSetup.kt 
package net.rafaeltoledo.social 

import android.app.Application 
import net.rafaeltoledo.social.di.viewModelModule 
import org.koin.android.ext.android.startKoin 
import org.koin.dsl.module.applicationContext 

class TestSocialApp : SocialApp() { 
  
  fun overrideStringValue(newValue: String) { 
    // Override value 
    // This behavior should be explicit in a future version of Koin 
    // See: https://github.com/Ekito/koin/pull/123 
    loadKoinModules(listOf( 
      applicationContext { bean { newValue } } 
    )) 
  } 
} 

Aqui, a ideia é deixar o valor a ser injetado configurável, para que possamos controlá-lo durante a execução dos testes. Assim, um teste que verifica se o valor inserido foi exibido corretamente na tela ficaria:

package net.rafaeltoledo.social 

import android.widget.TextView 
import com.google.common.truth.Truth.assertThat 
import org.junit.Test 
import org.junit.runner.RunWith 
import org.robolectric.Robolectric 
import org.robolectric.RobolectricTestRunner 
import org.robolectric.RuntimeEnvironment 

@RunWith(RobolectricTestRunner::class) 
class MainActivityTest { 
  
  @Test 
  fun checkIfActivityIsSuccessfullyCreated() { 
    // Arrange 
    val newValue = "Test Social App" 
    val app = RuntimeEnvironment.application as TestSocialApp 
    app.overrideStringValue(newValue) 
    val activity = Robolectric.setupActivity(MainActivity::class.java) 
    
    // Act - nothing to do 
    
    // Assert 
    val text = activity.findViewById(R.id.content) 
    assertThat(text.text).isEqualTo(newValue) 
  } 
}

Para deixar as asserções mais fluídas, estou utilizando o Truth.

Ao abrir um Pull Request para o repositório, temos, então, uma surpresa!

Pull Request

O Coveralls percebeu que tivemos uma queda na cobertura de código (de 100% para 65%) e nos avisou! Esse é exatamente o intuito de mapear essa métrica, para percebermos o quanto um determinado código está impactando na cobertura de testes. Por mais que ter uma cobertura de código alta não seja sinônimo de qualidade, ela pode indicar que estamos escrevendo código sem testar, o que definitivamente não é legal!

Nesse caso específico, tínhamos a utópica marca de 100% porque tínhamos apenas uma classe sem código algum, então a queda já era esperada.

Bom, por hoje é isso! O PR com essas modificações foi aberto, aprovado e mergeado. Vale salientar aqui que a implementação inicial teve uma crítica bem legal do Victor Nascimento. A ideia é exatamente essa, os PRs devem gerar discussões técnicas relevantes.

Até a próxima!