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