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

Padrão

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

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

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

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

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

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

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
}

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

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!

The Social Project: Configurando o Tema e Identificando suas Builds

Padrão

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

Olá pessoal! No último post configuramos o Firebase no projeto, ao mesmo tempo em que mantivemos os JSONs de configuração seguros no nosso CI. Hoje vamos começar a preparar nosso app para soltar as primeiras releases.

Trazendo para o projeto algumas experiẽncias que tive em projetos que participei, posso dizer que a diferenciação entre o app em tempo de desenvolvimento e as releases é um problema bastante menosprezado quando estamos trabalhando em um projeto. É muito comum chegarmos ao ponto de não saber ao certo qual é a versão que estamos utilizando, e a inconsistência de comportamento acaba por nos colocar em situações complicadas (como testar o app errado, inconsistência de dados, dentre vários outros).

O primeiro passo, a viabilidade de que tanto as builds de release quanto as de debug possam conviver harmônicamente no mesmo device já foi resolvido no post anterior, através do applicationIdSuffix para o build type de debug.

O que faremos agora é criar uma identidade visual diferente para cada um dos build types.

Aqui vai um aviso: como ainda não temos um design definitivo, ou uma identidade visual para o nosso aplicativo, vai ser algo meio grotesco mesmo, mais como um placeholder do que algo definitivo, ou apropriado para um produto final 🙂

Como ícone do aplicativo, utilizei um dos ícones do banco de ícones do Material Design. São ícones para os mais diversos propósitos, divididos em várias categorias, otimizados tanto para mobile quanto para web.

Sim. Esse é o ícone do nosso app! 😂

Para prosseguirmos, também utilizei a ferramenta de cores do site do Material Design, que me ajudou a encontrar uma combinação de cores com bom contraste e gerar as suas variações mais clara e mais escura. Assim, os nosso ícones ficaram:

Sendo o ícone da esquerda a versão do aplicativo de release e a versão da direita a versão do app de debug.

Aqui, basicamente a ideia foi inverter as cores primary e accent para cada um dos build types. Dessa forma, o aplicativo final terá o verde como primary e o laranja como accent, enquanto a versão de debug terá o laranja como primary e o verde como accent. (vamos ver como isso vai ficar ao longo do projeto – designers, me salvem aqui! 😂)

Para que essa façanha funcione no aplicativo, precisamos fazer uso das diferentes pastas de source. Como pode ser visto no repositório, basicamente eu dupliquei os resources relacionados ao tema e ao ícone, separando suas respectivas versões em src/debug/ressrc/release/res. É importante salientar que os resources devem sempre ter o mesmo nome e os mesmos atributos nos XMLs para as duas pastas, caso contrário teremos problemas ao compilar o aplicativo.

O Android Gradle Plugin (AGP) tem esse mecanismo bacana. Com o mecanismo de build types (e flavors), conseguimos criar variações de um mesmo app facilmente. Da forma como fizemos, o aplicativo de debug será criado mesclando o conteúdo de src/mainsrc/debug, enquanto o de release será criado com o conteúdo de src/mainsrc/release.

Bom, aproveitando, também atualizei algumas coisas no repositório, como o Kotlin para a versão 1.2.31, e o Google Play Services (e Firebase) para a versão 12.0.1. Também atualizei o AGP para a versão 3.1.0, dando suporte ao Android Studio 3.1.

Com isso, tanto nosso app em debug e em release podem conviver em harmonia dentro do mesmo dispositivo.

E é isso! Lembrando que o código já foi mergeado e está disponível na branch develop do projeto.

Até a próxima!

The Social Project: Configurando o Firebase no Projeto

Padrão

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

Olá pessoal! No último post configuramos as chaves de assinatura do nosso app de maneira segura, garantindo que os dados não estejam expostos no repositório e viabilizando que o CI consiga buildar e assinar os APKs. No post de hoje, vamos fazer a configuração do projeto no Firebase.

O primeiro passo é criarmos os projetos em nossa conta Google. Por que escrevi no plural? Porque faz todo sentido separarmos os ambientes, tendo um projeto para desenvolvimento (onde poderemos testar das mais diversas formas desde formato de dados, notificações, configurações e outros) e outro para produção, podendo assim realizar diversos experimentos sem impactar o app já publicado.

No meu caso, criei dois projetos no painel do Firebase, dando o nome de Social App DevelopmentSocial App Production.

Com os dois projetos criados, vamos configurar o nosso app. Primeiramente o build.gradle principal do projeto:

buildscript {

  ext.versions = [
      'kotlin': '1.2.30',
      'supportLibrary': '27.1.0',
      'googleServices': '12.0.0'
  ]
    
  repositories {
    google()
    jcenter()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:3.0.1'
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
    classpath 'com.google.gms:google-services:3.2.0'
  }
}

allprojects {
  repositories {
    google()
    jcenter()
  }

  configurations.all {
    resolutionStrategy {
      eachDependency { details ->
        if (details.requested.group == 'com.android.support'
            && details.requested.name != 'multidex'
            && details.requested.name != 'multidex-instrumentation') {
          details.useVersion versions.supportLibrary
        }
      }
    }
  }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Fizemos algumas boas mudanças no arquivo. Primeiramente criei um bloco para padronizar a configuração de versões que se repetem em múltiplos locais do projeto – a versão do Kotlin, da Support Library e das bibliotecas Google (Firebase e Play Services). Em seguida, adicionamos o plugin google-services em sua última versão, que se encarregará de configurar o nosso projeto.

Nesse arquivo, também, coloquei uma pequena configuração para forçar todas as Support Libraries a compartilharem da mesma versão. Por quê? Muitas vezes, ao adicionar uma outra dependência – por exemplo, o próprio Firebase – ele depende de uma versão de alguma Support Library mais antiga que a utilizada no projeto. Uma das formas de se resolver isso é dando um exclude na dependência e adicionando manualmente a versão mais recente. Particularmente não gosto dessa abordagem por poluir muito o bloco de dependências. Dessa forma, temos esse bloco de configuração que vai garantir que as Support Libraries estejam todas na versão 27.1.0 (exceto as bibliotecas de Multidex, que possuem um versionamento próprio).

Em seguida, vamos configurar o build.gradle do projeto app.

android {
  ...
  buildTypes {
    debug {
      applicationIdSuffix '.dev'
      signingConfig signingConfigs.debug
    }
    ...
  }
}

dependencies {
  implementation "com.android.support:appcompat-v7:$versions.supportLibrary"

  implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"

  implementation "com.google.firebase:firebase-core:$versions.googleServices"
  ...
}

apply plugin: 'com.google.gms.google-services'

Aqui, primeiramente, adicionamos um sufixo ao applicationId para o build type debug. Isso vai facilitar mantermos tanto a versão de produção quanto a de desenvolvimento no mesmo dispositivo. Em seguida, configurei as dependências para utilizar as versões definidas globalmente e adicionei a dependência firebase-core, que já nos adiciona o analytics, por exemplo. Por fim, aplicamos o plugin google-services no final do arquivo, conforme a documentação.

Bom, feito isso, vamos configurar os aplicativos no painel do Firebase.

Aqui, vamos inserir o nome do pacote (net.rafaeltoledo.social para o app de produção, net.rafaeltoledo.social.dev para o app de desenvolvimento), opcionalmente um apelido e o hash SHA-1 de assinatura. Podemos facilmente conseguir esse hash através da task signingReport do Gradle. Para executá-la, certifique-se de que a chave de produção esteja descriptografada e que as variáveis de ambiente com as senhas estejam exportadas.

Com o resultado dessa task, copie o hash SHA-1 de debug e release e coloque em cada um dos projetos. Com isso, teremos dois arquivos google-services.json, um para cada projeto.

Pronto, agora é só commitar e seguir o jogo? Não necessariamente…

Quando você está desenvolvendo um projeto, idealmente você não deveria deixar estes arquivos no seu controle de versão. Primeiramente, porque alguém poderia criar um app com o mesmo applicationId e utilizá-lo para acessar o seu projeto. Segundo, que poderia com isso também comprometer a sua cota de serviços e, caso você utilize um dos serviços pagos do Firebase, ocasionar em gastos para você. Portanto, trate o arquivo google-services.json também como uma chave do app.

Para resolver esse problema, primeiramente coloquei o arquivo no .gitignore, para que ele não seja adicionado ao Git. Em seguida, vamos colocar o conteúdo do arquivo no CI, para que sejam colocados no projeto na hora do build. Farei isso colocando o conteúdo em uma variável de ambiente e, através do comando echo, mandando o conteúdo pra um arquivo.

O conteúdo desse arquivo é simplesmente um JSON, então a minha recomendação é, antes de criar as variáveis, utilizar algum serviço de minificação (ou minify) para remover espaços e quebras de linha desnecessárias.

Por fim, basta adicionar uma etapa no arquivo de configuração do CI para obter o valor da variável e jogar no arquivo corretamente dentro de cada uma das pastas (app/src/release app/src/debug).

- 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"

E é isso. Configuramos corretamente o projeto e mantivemos nossos segredos em segurança 🙂

O código já foi mergeado no repositório após um PR e a build no CI 🙂

The Social Project: Configurando a Assinatura do App no CI

Padrão

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

Olá pessoal! No último artigo, configuramos o nosso build script para suportar o Kotlin. No post de hoje, vamos configurar a assinatura do app e levantar um ponto importante: como guardar seus segredos, principalmente quando o app é open source?

Com certeza, se você procurar um pouco, você encontrará diversos repositório com todo tipo de informação privada, desde chaves de APIs a ferramentas pagas. Todas, commitadas de forma acidental (ou mesmo proposital, mas de forma inadvertida) no repositório público do projeto.

No Android, um dos maiores “segredos” que mantemos é a chave de assinatura do app. Ela, juntamente com o Application ID, fazem um app se tornar único. Como o identificador é público, o vazamento de uma chave pode gerar um seríssimo risco de segurança, possibilitando que versões modificadas do seu app sejam passíveis de instalação sobre o app original, não só possibilitando comportamentos potencialmente perigosos para o usuário, como o acesso a informações internas e privadas que eventualmente o seu app tenha salvo localmente (como dados de autenticação na sua API, por exemplo).

A perda dessa chave também pode ser um problemão. Sem ela, é impossível enviar atualizações do seu app para o Google Play, por exemplo. Hoje felizmente já temos formas de minimizar esse risco, utilizando o Google Play App Signing (assunto que com certeza será abordado nessa série de posts).

Falando especificamente do nosso app, temos então um problema com relação a isso. Para a assinatura, precisamos de um arquivo (geralmente com a extensão .keystore ou .jks), duas senhas e um alias. O primeiro questionamento é sobre o arquivo em si. Colocaremos ele no repositório? Por mais que a chave necessite das senhas, ter o arquivo exposto envolve algum grau de risco.

Para solucionar esse problema temos basicamente duas abordagens: a primeira delas seria colocar o arquivo de assinatura em algum serviço externo, como Google Drive ou Dropbox, e baixá-lo no momento da build utilizando curl. Nesse cenário, teríamos uma chave de API exposta via variável de ambiente. Apesar de viável, esse cenário tem o problema de estarmos acoplando uma dependência externa ao simples processo de build do APK. Além de oscilações de rede que podem causar falha da build, também temos que manter essa request em dia, tendo que ajustar a chamada de download no caso de eventuais mudanças nas APIs desses serviços.

A segunda abordagem, e é a que vou utilizar aqui, será criptografar o arquivo e colocá-lo no repositório junto com o projeto. No CI, teremos exportado como variável de ambiente a chave que consegue descriptografar o arquivo, bem como as outras chaves necessárias para assinar o APK.

A maneira mais simples de gerarmos uma chave é através do próprio Android Studio.

Com a chave gerada, utilizaremos o openssl para criptografar o arquivo. Podemos fazer isso utilizando o comando:

openssl aes-256-cbc -e -in release.keystore -out release.keystore-cipher -md sha256 -k $CIPHER_DECRYPT_KEY

Perceba que no comando temos uma variável chamada CIPHER_DECRYPT_KEY, que é a chave que irá descriptografar o nosso arquivo lá no CI.

Com o arquivo criptografado gerado, vamos criar uma pasta no projeto para guardar as chaves. Além do arquivo criptografado release.keystore-cipher, também estou colocando o arquivo que assina as releases de debug. Isso é particularmente útil quando existe mais de uma pessoa trabalhando no projeto ou quando eventualmente há a necessidade de se trocar ou formatar a máquina, já que essa chave é gerada sempre que uma nova instalação da SDK do Android é realizada.

Com os arquivos no projeto, vamos primeiramente dizer ao CI para descriptografar o arquivo antes de executar a build. Perceba que adicionamos um passo chamado Decrypt release key ao nosso arquivo config.yml. Também modifiquei o passo de build para chamar a task assemble em vez da build (para validar a parte de assinatura de ambos os build variants):

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: Build
          command: ./gradlew clean check assemble assembleAndroidTest

      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}

Feito isso, vamos ao nosso build script configurar a assinatura e atribui-las para cada um dos build variants:

...

android {
  ...
  signingConfigs {
    debug {
      storeFile file("$rootDir/distribution/debug.keystore")
      storePassword 'android'
      keyAlias 'androiddebugkey'
      keyPassword 'android'
    }

    release {
      storeFile file("$rootDir/distribution/release.keystore")
      storePassword System.env.RELEASE_STORE_PASSWORD
      keyAlias System.env.RELEASE_KEY_ALIAS
      keyPassword System.env.RELEASE_KEY_PASSWORD
    }
  }

  buildTypes {
    debug {
      signingConfig signingConfigs.debug
    }

    release {
      signingConfig signingConfigs.release
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}

...

Perceba que a parte da assinatura de release vai pegar também os dados através de variáveis de ambiente, que serão adicionadas ao CircleCI:

E é isso! O Pull Request foi aberto e aprovado para este caso, tendo passado no CI e integrado na branch develop do repositório.

No próximo post vamos fazer a configuração do Firebase no projeto 🙂

The Social Project: Configurando Kotlin no projeto

Padrão

Olá pessoal!

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

No último post, configuramos o CircleCI para observar as branches do nosso projeto, bem como a abertura de pull requests no repositório. Hoje, vamos olhar um pouco mais o projeto e começar a estruturá-lo para posteriormente iniciarmos a implementação de fato.

O projeto em si foi criado a partir do template padrão do Android Studio 3.0.1, sem qualquer modificação na sua estrutura, e sem a inclusão do Kotlin por padrão. Meu intuito é entendermos o que é necessário para transformarmos um projeto Android com Java em um projeto Android com Kotlin.

O primeiro passo aqui é ajustar o arquivo build.gradle principal, aquele localizado na raiz do projeto. Inicialmente, vamos remover os comentários vindos do template. Eles são informativos sobre como configurar as dependências do projeto, porém podemos removê-los tranquilamente, já que sabemos onde vamos colocar cada coisa.

Em seguida, vamos criar uma variável, chamada de kotlinVersion, para guardar a versão do Kotlin que estaremos utilizando no projeto. É importante que essa versão esteja separada, pois ela deve ser a mesma em todas as dependências relacionadas ao Kotlin no projeto. Aqui fica uma ressalva: perceba que utilizamos a nomenclatura com Camel Case (enquanto o template do AS para Kotlin utiliza Snake Case). O Groovy (linguagem utilizada nos scripts do Gradle) tem convenções de código muito semelhantes ao Java. Assim, utilizamos Snake Case apenas nos casos onde o valor é uma constante, e sempre em caixa alta.

Após isso, adicionamos o plugin de Kotlin para Gradle, fazendo uso da interpolação de strings para inserir a versão. Assim, nosso arquivo build.gradle ficará assim:

buildscript {

    ext.kotlinVersion = '1.2.30'
    
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Vamos agora editar então o arquivo de build do módulo app (localizado em app/build.gradle). Nele, primeiramente aplicamos o plugin do Kotlin, seguindo a nomenclatura mais recomendada pelo Gradle – utilizando o namespace. Dessa forma, aplicamos o plugin org.jetbrains.kotlin.android. Em seguida, vamos atualizar o tooling para a última versão do Android (atualmente a API 27). Como o Android Studio 3.0 já foi lançado a algum tempo, o seu template padrão ainda aponta para a API 26. Assim, apontamos as versões de compileSdkVersiontargetSdkVersion para 27, além de atualizarmos a versão da Support Library para 27.1.0, a versão mais recente neste momento.

O próximo passo para completar a integração do Kotlin no projeto é a adição da standard library do Kotlin. Essa dependência possui 3 versões: kotlin-stdlib (Java 6), kotlin-stdlib-jre7 (Java 7) e kotlin-stdlib-jre8 (Java 8). Particularmente eu adiciono a versão jre7, primeiro pelo fato da nossa API mínima ser KitKat (que já suporta as features do Java 7). Poderíamos utilizar a jre8, porém isso faria com que o processo de desugaring ocorresse durante a compilação, o que pode impactar no nosso tempo de build.

Por fim, dois pontos de ajuste que particularmente recomendo por questões de organização. O primeiro é a adição de source sets específicos para Kotlin. Quando temos um projeto Java, os arquivos fonte geralmente ficam na pasta src/main/java. Assim, adicionamos aos source sets src/main/kotlin. Ao meu ver, essa organização é particularmente útil quando o projeto acaba tendo arquivos de ambas as linguagens (o que não deve ocorrer no nosso caso) e para que a estrutura de diretórios fique mais semântica. O segundo ponto de ajuste, mais estético, é a padronização do uso de aspas simples e duplas no arquivo. Aspas duplas, somente quando houver interpolação na string.

Nosso arquivo, então, fica dessa forma:

apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'

android {
    compileSdkVersion 27

    defaultConfig {
        applicationId 'net.rafaeltoledo.social'
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName '1.0'

        testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        test.java.srcDirs += 'src/test/kotlin'
        androidTest.java.srcDirs += 'src/androidTest/kotlin'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation 'com.android.support:appcompat-v7:27.1.0'

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

Aproveitando que estamos modificando as configurações de build do nosso projeto, recomendo atualizarmos também a versão do Gradle que está sendo utilizada. Para isso, basta gerarmos o wrapper apontando para a versão mais recente (4.6 no momento em que escrevo este post). A não ser quando há imcompatibilidades entre o plugin do Android e o Gradle, sempre recomendo a utilização da versão mais recente do Gradle, já que ela costuma trazer melhorias importantes de performance (o que é sempre bem-vindo). Para atualizar o wrapper, basta executar o seguinte comando:

./gradlew wrapper --gradle-version 4.6 --distribution-type all

Para que nosso aplicativo já tenha uma Activity inicial, também adicionei uma MainActivity vazia e adicionei ao Android Manifest. Também aproveitei e exclui os testes que vem por padrão no template do Android Studio – assim que começarmos a entrar nas features do app, escreveremos os nossos 🙂

Seguindo o nosso Git Flow, criei um Pull Request para a branch develop, que, assim que passou no CI, foi mergeado 🙂

Por hoje é isso. Apesar deste post não mostrar nada muito novo, acho importante configurarmos o Kotlin por nós mesmos e saber o que muda no projeto. Afinal, os build scripts também são código, e mantê-los organizados faz parte da saúde do projeto. Aguardem que no próximo post teremos mais coisas interessantes.

Até lá!

The Social Project: configurando o servidor de CI

Padrão

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

No post anterior, criamos o repositório com um projeto vazio e configuramos o repositório com as branches persistentes que farão parte do nosso Git Flow.

Para essa tarefa, escolherei o CircleCI. Por quê? Primeiramente pelo seu suporte a Docker, o que facilita e muito na hora de fazer o setup do ambiente, caso a gente precise customizar o tooling. Além disso, ele possui uma cota gratuita generosa não só para projetos open-souce (que está sendo o caso deste daqui), mas também para projetos privados no Github ou Bitbucket.

Bom, primeiramente faremos o login no CircleCI com a nossa conta do Github e faremos o setup do nosso repositório.

Na tela seguinte, selecionamos uma máquina Linux e o Gradle como ferramenta de build. Nessa mesma tela, será oferecido um arquivo de configuração default, a ser colocado na pasta .circleci do repositório, com o nome de config.yml. O conteúdo inicial do arquivo é:

# Java Gradle CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-java/ for more details
#
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/openjdk:8-jdk
      
      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/repo

    environment:
      # Customize the JVM maximum heap limit
      JVM_OPTS: -Xmx3200m
      TERM: dumb
    
    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "build.gradle" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run: gradle dependencies

      - save_cache:
          paths:
            - ~/.gradle
          key: v1-dependencies-{{ checksum "build.gradle" }}
        
      # run tests!
      - run: gradle test

A partir desse modelo, faremos algumas alterações. A primeira delas é mudar a imagem do docker para uma que já contenha todo o setup para que possamos buildar apps Android – temos uma lista das imagens mantidas pelo próprio CircleCI aqui. Além disso, configuramos a versão da JDK a ser utilizada e configuramos o cache de dependências (para que não precise ser baixadas novamente, caso não haja mudança nos arquivos do Gradle). Colocamos também uma task preventiva (Accept licenses) para evitar que o processo de build quebre por conta de licenças não aceitas.

Na task de build, configuramos a execução do Lint (através da task check), a compilação e execução de testes locais (através da task build) e, por enquanto, a compilação dos testes instrumentados (através da task assembleAndroidTest) – posteriormente faremos a delegação da execução desses testes para o Firebase Test Lab.

O arquivo final fica assim:

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: Build
          command: ./gradlew clean check build assembleAndroidTest

      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}

Feito isso, também colocarei um badge no README.md do repositório, indicando o atual status da nossa build. Tudo isso será então commitado em uma branch chamada rt/setup-ci e então um pull request será aberto.

Como sou administrador, eu poderia simplesmente ignorar o fluxo e eu mesmo mergear meu Pull Request. Porém, iria quebrar o fluxo que eu mesmo estou propondo! 🙂

Para fazer o review do meu PR, adicionei a Elessandra como revisora. No caso, a pessoa que revisa o PR pode fazer apontamentos e, caso esteja tudo certo, aprovar e mergear.

Como nem tudo são flores, eu commitei o arquivo com alguns erros de digitação na primeira tentativa. Como ainda não tínhamos o CI integrado para validar o arquivo de configuração, os olhos humanos deixaram passar. Com isso, fui obrigado a fazer outro PR revertendo a modificação. Pela própria interface do Github é possível solicitar desfazer o PR.

Corrigidos os erros (e, após algumas tentativas), finalmente consegui configurar o CI corretamente.

Como foram vários commits de tentativa e erro, o PR ficou com vários commits. Para esse caso, o ideal é que se faça o Squash (juntar todos os commits em um único), evitando assim que o histórico do Git seja poluído.

Por fim, após o merge é importante excluir a branch de origem, já que ela não é uma branch persistente. Assim, mantemos o repositório limpo e organizado 🙂

Por hoje é isso. O repositório já com as modificações está disponível aqui.