The Social Project: Configurando a Koin para DI

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',
]
...
}
view raw snippet01.groovy hosted with ❤ by GitHub
// 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"
}
view raw snippet02.groovy hosted with ❤ by GitHub

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
}
view raw snippet03.kt hosted with ❤ by GitHub

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()
})
}
}
view raw snippet04.kt hosted with ❤ by GitHub

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

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" }
}
view raw snippet05.kt hosted with ❤ by GitHub
// 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()) }
}
view raw snippet06.kt hosted with ❤ by GitHub

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
))
}
}
view raw snippet07.kt hosted with ❤ by GitHub

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, viewModelModulefirstModule. É importante lembrar de adicionar a nossa implementação da Application ao AndroidManifest.

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

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 } }
))
}
}
view raw snippet08.kt hosted with ❤ by GitHub

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)
}
}
view raw snippet09.kt hosted with ❤ by GitHub

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!

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!

The Social App: Integrando dados de Cobertura de Código

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

Olá pessoal! No último post realizamos uma configuração inicial do tema do nosso aplicativo de forma a diferenciar as builds de debug e release. No post de hoje, vamos configurar uma métrica importante para o desenvolvimento de qualquer projeto de software, a cobertura de código.

Simplificando ao máximo o conceito (já que a explicação dele foge um pouco ao escopo deste post), a métrica diz respeito a quanto do seu código é “exercitado” durante a execução dos testes. Os relatórios de cobertura geralmente fornecem formas de mensurar não só percentualmente quanto do código foi coberto pelos testes, mas também pode dar insights sobre quais partes do código poderiam receber mais testes.

O primeiro passo é habilitarmos a geração desse dado no projeto. Porém, neste momento não é possível gerar este dado, já que nosso projeto não possui nenhum teste. Na verdade, ele praticamente não possui código, somente uma Activity vazia que ainda não faz nada. Poderíamos criar um teste simples de Espresso, para validar a inicialização da Activity, porém ainda não resolvemos o problema da execução de testes instrumentados no nosso CI (inclusive, se você quer ler mais sobre a diferença entre testes instrumentados e locais, tem um post excelente do Victor Nascimento no Medium). Dado esse cenário, vou utilizar o Robolectric para criar um teste que, apesar de utilizar classes do Android, roda localmente na JVM.

Para integrarmos o Robolectric em nosso projeto, é necessário adicionar a sua dependência, dentro do escopo de testes (testImplementation). Além disso, como o Robolectric utiliza resources do Android, precisamos habilitar a disponibilização dos resources para os testes unitários. Caso contrário, nossos testes falharão por não encontrar qualquer referência a resources adicionados por nós no projeto (como layouts, strings, drawables e outros).

android {
...
testOptions {
unitTests.includeAndroidResources true
}
}
dependencies {
...
testImplementation 'org.robolectric:robolectric:3.8'
}
view raw snippet01.groovy hosted with ❤ by GitHub

Dentro da pasta src/test/kotlin, vamos criar uma classe de teste bem simples, chamada MainActivityTest, que simplesmente fará uma validação se a Activity foi criada com sucesso pelo Robolectric. Apesar de ser um teste sem muito valor para o app (afinal, estamos testando se o Robolectric funciona), esse teste gerará alguma cobertura para o nosso projeto.

package net.rafaeltoledo.social
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class MainActivityTest {
@Test
fun checkIfActivityIsSuccessfullyCreated() {
assertNotNull(Robolectric.setupActivity(MainActivity::class.java))
}
}
view raw snippet02.kt hosted with ❤ by GitHub

Com o teste criado (e passando!), vamos configurar a geração dos dados de cobertura. Eu já escrevi dois posts sobre o assunto, então não vou me alongar muito sobre os detalhes de implementação aqui para não deixar o este post muito longo 🙂

Criei um script chamado coverage.gradle, que coloquei na pasta gradle/ do projeto (inicialmente, tinha colocado numa pasta chamada tools/, mas após algumas discussões a respeito, pareceu fazer mais sentido a primeira opção).

apply plugin: 'jacoco'
jacoco.toolVersion versions.jacoco
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
def classes = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug")
def sources = files("$projectDir/src/main/kotlin")
def report = "$buildDir/reports/jacoco/report.xml"
task createCombinedCoverageReport(type: JacocoReport,
dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
sourceDirectories = sources
classDirectories = files(classes)
executionData = fileTree(dir: buildDir, includes: [
'jacoco/testDebugUnitTest.exec',
'outputs/code-coverage/connected/*coverage.ec'
])
reports {
xml.enabled = true
xml.destination file(report)
html.enabled = true
}
}
view raw snippet03.groovy hosted with ❤ by GitHub

Bom, a parte de configuração do relatório em si é muito próxima do que expliquei nos dois posts sobre o assunto. Algumas coisas estão isoladas em variáveis (sourcesreport), pois vamos reutilizá-las já já.

Com isso, vamos incluir o arquivo no build.gradle do módulo app e habilitar a geração de cobertura de código para os testes instrumentados:

apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
apply from: "$rootDir/gradle/coverage.gradle"
android {
...
buildTypes {
debug {
testCoverageEnabled true
...
}
...
}
}
...
view raw snippet04.groovy hosted with ❤ by GitHub

Para finalizar, vamos setar a versão do jacoco no classpath para a última versão, modificando o build.gradle na raiz do nosso projeto:

buildscript {
ext.versions = [
'kotlin': '1.2.31',
'supportLibrary': '27.1.1',
'googleServices': '12.0.1',
'jacoco': '0.8.1'
]
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath 'com.google.gms:google-services:3.2.1'
classpath "org.jacoco:org.jacoco.core:$versions.jacoco"
}
}
view raw snippet05.groovy hosted with ❤ by GitHub

Feito isso, já podemos executar a task createCombinedCoverageReport e deveremos obter o relatório com incríveis 100% de cobertura, disponível dentro da pasta app/build/reports/jacoco

Perceba que ele foi gerado pela versão correta do Jacoco que configuramos, 0.8.1.

Com a métrica sendo gerada, é importante que ela esteja visível dentro do nosso fluxo de trabalho, para que possamos acompanhar a sua evolução ao longo do desenvolvimento do projeto. Para isso, precisamos “publicar” essa informação em algum lugar.

O Jenkins, por exemplo, oferece formas de acompanhar isso, caso que não ocorre com o CircleCI. Para isso, vamos utilizar um serviço externo, no caso o Coveralls. Durante o desenvolvimento, validei também o Codecov, porém ele não se comportou muito bem com o Kotlin. O Codecov irá se plugar ao nosso fluxo, validando a cobertura atual em cada Pull Request, e exibirá um histórico de como essa métrica evolui a cada commit.

Para que essa integração funcione, precisamos de enviar essa métrica para o Coveralls. Faremos isso através de um plugin. A configuração dele é bem simples:

// build.gradle
buildscript {
...
repositories {
google()
jcenter()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath 'com.google.gms:google-services:3.2.1'
classpath "org.jacoco:org.jacoco.core:$versions.jacoco"
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'
}
}
view raw snippet06.groovy hosted with ❤ by GitHub

No arquivo coverage.gradle, faremos a aplicação do plugin, bem como a sua configuração:

// coverage.gradle
apply plugin: 'jacoco'
apply plugin: 'com.github.kt3k.coveralls'
...
def classes = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug")
def sources = files("$projectDir/src/main/kotlin")
def report = "$buildDir/reports/jacoco/report.xml"
...
coveralls {
sourceDirs = sources.flatten()
jacocoReportPath = report
}
view raw snippet07.groovy hosted with ❤ by GitHub

Com isso, o último passo é configurar a chave de upload dos relatórios no CI. Para isso, basta criarmos uma variável de ambiente chamada COVERALLS_REPO_TOKEN e colocar o valor fornecido no painel do Coveralls.

Feito isso, o último passo é editar o nosso arquivo de configuração do CI para incluir algumas coisas:

version: 2
jobs:
build:
docker:
- image: circleci/android:api-27-alpha
working_directory: ~/social-app
environment:
JVM_OPTS: -Xmx3200m
CIRCLE_JDK_VERSION: oraclejdk8
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Accept licenses
command: yes | sdkmanager --licenses || true
- run:
name: Decrypt release key
command: openssl aes-256-cbc -d -in distribution/release.keystore-cipher -out distribution/release.keystore -md sha256 -k $CIPHER_DECRYPT_KEY
- run:
name: Setup Google Services JSON
command: |
mkdir -p app/src/debug/ && touch app/src/debug/google-services.json
echo "${JSON_FIREBASE_DEVELOPMENT}" >> "app/src/debug/google-services.json"
mkdir -p app/src/release/ && touch app/src/release/google-services.json
echo "${JSON_FIREBASE_RELEASE}" >> "app/src/release/google-services.json"
- run:
name: Run Linters
command: ./gradlew check
- run:
name: Run Tests and generate Code Coverage
command: ./gradlew createCombinedCoverageReport
- run:
name: Upload code coverage data
command: ./gradlew coveralls
- run:
name: Build
command: ./gradlew assemble assembleAndroidTest
- store_artifacts:
path: app/build/reports/jacoco/createCombinedCoverageReport
destination: coverage-report
- store_artifacts:
path: app/build/reports/tests/testDebugUnitTest
destination: local-test-report
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
view raw snippet08.yaml hosted with ❤ by GitHub

Aqui eu fiz algumas modificações. Primeiramente, separei as tasks de linters, testes e build (a partir da linha 35), adicionando a task que vai enviar os dados para o Coveralls (Upload code coverage data). Além disso, comecei a disponibilizar no CircleCI os relatórios e de cobertura e execução de testes (utilizando a configuração store_artifacts). Com isso, após a execução de uma build com sucesso, é possível navegar por esses artefatos, desde que esteja logado.

Ufa! É isso! Você pode conferir no repositório o resultado dessas mudanças. A partir de agora, a cobertura de código é exibida também no README do projeto e como uma etapa de verificação nos PRs abertos!