Skip to content

Android DEV FAQ

Renan Barbieri edited this page Mar 2, 2021 · 14 revisions

O objetivo do DEV FAQ é facilitar o acesso ao histórico de decisões que foram tomadas no que diz respeito às boas práticas do projeto e a justificativa das decisões.

Qual será o padrão de objeto para trafegar erros no ViewState e no ResultWrapper

  • Depende de cada caso, mas respeitando os modelos de cada camada
  • Todo erro trafegado do domínio (UseCase) para a apresentação (ViewModel) deve ser BaseErrorData ou BaseErrorStatus
  • Exemplos:
    • ViewState pode trafegar modelos de domínio e de UI.
    • O ResultWrapper da camada de domínio só pode trafegar modelos de domínio
    • O ResultWrapper da camada de dados pode trafegar modelos de domínio e de dados

Onde será feito o Mapper de cada camada?

  • A camada de domínio não possui mapper.
  • Mapper na camada de dados:
    • O mapeamento para o modelo de dominio chamado na classe de implementação do repositório (ExampleRepositoryImpl)
  • Mapper na camada de apresentação:
    • O mapeamento para o modelo de de UI é feito no ViewModel
  • O método responsável pelo mapper fica na classe de modelo de UI ou de dados, mas em casos que não é possivel executar testes (como por exemplo em modelos do ActiveAndroid), é necessário criar uma classe ExampleToDomainMapper que poderá extender de Mapper.

Vamos trafegar FeatureFlag até onde nos casos de features com isso?

Devemos trafegar até a camada de domínio onde o UseCase interpreta a feature e responde para o ViewModel com um erro de FEATURE_FLAG_DISABLED caso a feature esteja desabilitada.

  • Em caso de feature habilitada, o caso de uso utiliza essa informação para executar as regras de negócio (não é necessário criar um caso de uso só para verificar se a feature está habilitada)
  • Exemplo:
    fun run(params: Params): ResultWrapper<String, BaseErrorStatus> {
        val remoteConfig = exampleRepository.getRemoteConfigFeature()
    
        return if (remoteConfig.enabled) {
            //Do something important here
        } else {
            ResultWrapper(error = BaseErrorStatus.FEATURE_FLAG_DISABLED)
        }
    }

Um UseCase pode conhecer outro UseCase?

Não. Cada UseCase possui sua própria regra de negócio e recebe por parâmetro ou via repositório os dados que precisa.


Sempre dever existir um mapeamento DomainModel > UIModel?

Não. Apenas quando necessário.


Como será o padrão de casos em que na View, após o click, podemos seguir dois caminhos em função de um state do ViewModel (Ex.: UserIsFree?)

  • Utilizar SingleLiveEvent, que é um MutableLiveData que responde ao observador apenas uma vez:
    //val declaration at ViewModel
    val goToSubscriptionEvent = SingleLiveEvent<String>()
    
    //val usage at Activity
    viewModel.goToSubscriptionEvent.observe(this, Observer {
      //Do something
    })
  • A tomada de decisão sobre qual tela abrir fica no ViewModel. Cada tela a ser aberta é representada por um event

E em casos de envio de analytics...?

  • Utilizamos o padrão de projeto iterator para analytics. Desta forma, quem dispara o evento não precisa saber para quem está disparando.
    • Aqui há uma explicação de como funciona o padrão de projeto.
  • Podemos chamar o analytics tanto no ViewModel quanto na Activity
    • Aqui, devemos analisa caso a caso para entender as necessidades do tracking.
    • Usar preferencialmente na Activity

Objetos recebidos como params nos UseCases devem ser sempre quebrados em atributos (User, por exemplo)?

  • Sempre verificar se já existe um caso de uso que traga as informações que precise, se sim, receber via parâmetro
  • Não é uma regra receber todos os atributos de uma classe por parâmetro, nos casos em que precise do objeto inteiro, não há problema receber o objeto como parâmetro

Como funciona a passagem de parâmetros para os casos de uso?

  • Deve ser criada uma classe Params para representar o modelo de parametros que será passada no construtor e na função run
  • Esta data class deverá estar localizada dentro da classe do caso de uso
      fun ExampleUseCase(exampleRepository: ExampleRepository): BaseUseCase<ResultWrapper<..>, ExampleUseCase.Params> { 
        fun runSync(params: ExampleUseCase.Params): ResultWrapper<..> {
    
        }
    
        data class Params (
          val exampleVariable: Int
        )
      }

Como será o padrão de organização dos módulos do Koin?

  • Se for fornecido uma classe da interface solicitada, usar
    factory<ExampleRepository> {
      ExampleRepositoryImpl(...)
    }
  • Caso não haja uma interface a ser implementada, usar
    factory {
      ExampleLocalDataSource(...)
    }
  • Usar get() as Example ao invés de named_param = get() devido a erro de compilação vs erro de execução
    factory {
        ExampleUseCase(get() as ExampleRepository)
    }

Check de Conectividade no ViewModel?

Não, é feita através do UseCase. O módulo platform se comunica com os UseCases a partir de interfaces (igual aos repositórios da camada de dados)


Qual será o padrão de nomenclatura para as CODE REGIONS?

  • Letras maiúsculas separadas por "espaço"
  • Deve agrupar duas ou mais funções relacionadas ao mesmo escopo
     //region EXAMPLES
     fun exampleOne() {}
    
     fun exampleTwo() {}
     //endregion

Regions no topo da Fragment ou Activity?

Seguir padrão do Kotlin: https://kotlinlang.org/docs/reference/coding-conventions.html#class-layout

  • Exceto o posicionamento do companion object, que ficará no começo da classe.

Vamos usar a coding conventions do Kotlin?

Sim


Como devem ser os modelos do remote config (firebase)?

  • Se houver possibilidade de desabilitar a funcionalidade remotamente, o modelo deve possuir o seguinte padrão:
    { 
        "enabled":  true,
        "data": { 
            //caso haja algum outro conteúdo. Pode ser um objeto ou uma lista
        }
    }
  • Se a feature estiver desabilitada, o modelo deve seguir o mesmo, com o valor "data" vazio

Como será a passagem de parâmetros entre Activities?

  • Para a troca de activities, será necessário criar um companion object com um método openActivity(<params>?) na classe destino desejada.
  • A troca de activity (a Intent em si) é encapsulada na classe Navigator.
//Exemplo com parâmetro
class CurrentActivity1 : AppCompatActivity() {
  companion object {
      const val ACTIVITY_PARAM_KEY = "package.to.activity.param_key"

      fun openActivity(fromActivity: Activity, activityParam: Any) {
          val params = Bundle()
          params.putAny(ACTIVITY_PARAM_KEY, activityParam)

          Navigator.goToActivity(fromActivity, CurrentActivity1::class.java, params)
      }
  }
}

//Exemplo sem parâmetro
class CurrentActivity2 : AppCompatActivity() {
  companion object {
      fun openActivity(fromActivity: Activity) {
          Navigator.goToActivity(fromActivity, CurrentActivity2::class.java)
      }
  }
}

Como vamos tratar casos de status HTTP específicos

Já temos encapsulado no object ApiResponseHandler o tratamento para HTTP status que foram mapeados na enum class StatusType. A tradução de um StatusTypepara algum erro deverá ser feita na camada de repository


Como nomear e onde armazenar as constantes de extras?

As constantes de extras ficarão dentro do companion object, conforme a CurrentActivity1 do tópico "passagem de parâmetros entre activities".


Quais são as regras para adicionarmos log na aplicação?

Os logs servem para contar a história do usuário dentro da aplicação. Podemos colocar logs em todas as camadas do projeto, só devemos tomar cuidado para não haver redundãncia desnecessária de informação. Em casos em que encontramos cenários inesperados e que tratamos de alguma forma que não está mapeada na funcionalidade, lançamos uma excessão não fatal para o Firebase através da biblioteca Timber.


Temos um naming convention para nomes de resources?

  • Para cores:
    <prefixo produto>_<hexadecimal da cor sem #>
  • Para strings:
    <onde>_<o que é>
  • Para imagens:
    • Se for uma imagem, ícone, vector ou shape específico de uma determindada tela, utilizar o padrão <onde>_<o que é>
    • Se for uma imagem, ícone, vector ou shape genérico (utilizado em mais de uma tela), utilizar o padrão <o que é>_<característica(s)>_<cor, opcional>
  • Para ids:
    • <onde é>_<tipo de view>_<o que é> . Exemplo: paywall_imageview_close

Sempre que tivermos warnings numa classe alterada, precisamos remover todos?

Temos que realizar análise caso a caso, porém temos sempre como objetivo remover todos os warnings.


Qual será o padrão de nomenclatura dos models?

  • presentation: ExampleUIModel
  • domain: ExampleModel
  • data source: ExamplePayloadModel para modelos de envio de dados e ExampleResponseModelpara modelos de resposta.
  • banco de dados: ExampleEntity (ainda será revisitado)

Qual será o padrão para mocks

Os objetos de mock deverão ser sempre um mockk() com os valores esperados simulados. Há duas formas de declarar um mock:

  • Como uma classe que recebe em seu construtor um mock do objeto a ser simulado
    • Cada funcão dessa classe deverá representar um estado do objeto e deve ser auto-contida, ou seja, não deverá receber parâmetros para a construção do objeto
    • Normalmente é utilizado para mocks de objetos que são passados em construtores via injeção de dependência
    • Exemplo:
    //Declaração
    class MockExampleClass(val mock: ExampleClass) {
    
      fun mockExampleState1() {
          coEvery {
              mock.classFunction()
          } returns function.expectedObject1
      }
    
      fun mockExampleState2() {
          coEvery {
              mock.classFunction()
          } returns function.expectedObject2
      }
    }
    
    //Utilização
    class ExampleTests {
    
      private lateinit var mockExampleClass: MockExampleClass
    
      (...)
      @Before
      fun setup() {
        mockExampleClass = MockExampleClass(mocck())
    
        viewModelExample = ExampleViewModel(
          exampleClass = mockExampleClass.mock
        )
      }
    
      @Test
      fun doSomeTest() {
        //Arrange
        mockExampleClass.mockExampleState1()
    
        //Act
        val functionResult = viewModelExample.functionExampleState1()
    
        //Assert
        assert(...)
      }
    }
  • Como um objectque possui funções que retornam os mocks
    • Cada funcão dessa classe deverá representar um estado do objeto e deve ser auto-contida, ou seja, não deverá receber parâmetros para a construção do objeto
    • Normalmente é utilizado para parâmetros de função e para mockar classes de modelo (ExampleUIModel, ExampleModel, ExamplePayloadModel, ExampleResponseModel, ExampleEntity)
    • Exemplo:
    //Declaração
    object MockExampleModel {
      fun mockState1(): ExampleModel {
          val userMock: ExampleModel = mockk()
    
          every {
              userMock.login
          } returns "user.example"
          every {
              userMock.avatarUrl
          } returns "http://avatar.url"
    
          return userMock
      }
    }
    
    //Utilização
    class ExampleTests {
      @Test
      fun doSomeTest() {
        //Arrange
        val exampleParam = MockExampleModel.mockState1()
    
        //Act
        val functionResult = viewModelExample.functionExampleState1(exampleParam)
    
        //Assert
        assert(...)
      }
    }

Como vamos implementar bibliotecas?

Com o objetivo de ter uma única versão por biblioteca, centralizamos as versões no arquivo dependencies.gradle. Lá definimos todas as versões utilizadas, como:

  • minSDK
  • targetSDK
  • compileSDK
  • buildTools
  • buildToolsVersion
  • versões de todas as bibliotecas utilizadas nos módulos

Para não perdemos a facilidade de gerenciar as versões das bibliotecas e saber quando uma está desatualizada usando o warning do Android Studio, só unificamos a versão da biblioteca. O diretório dela, com.example.lib:lib-core, deverá ser escrito em cada módulo.

Exemplo:

//dependencies.gradle
ext {
  //Android Config
  minSDK = 21
  targetSDK = 29
  compileSDK = 29

  buildTools = '4.1.1'
  /* (...) */

  //Gson
  gsonVersion = '2.8.5'

  //Billing
  googlePlayBillingVersion = '2.1.0'
}

//build.gradle (app)
dependencies {
  implementation "com.android.billingclient:billing:$googlePlayBillingVersion"

  implementation "com.google.code.gson:gson:$gsonVersion"
}

Clone this wiki locally