-
Notifications
You must be signed in to change notification settings - Fork 4
Android DEV FAQ
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.
- Depende de cada caso, mas respeitando os modelos de cada camada
- Todo erro trafegado do domínio (
UseCase) para a apresentação (ViewModel) deve serBaseErrorDataouBaseErrorStatus - Exemplos:
-
ViewStatepode trafegar modelos de domínio e de UI. - O
ResultWrapperda camada de domínio só pode trafegar modelos de domínio - O
ResultWrapperda camada de dados pode trafegar modelos de domínio e de dados
-
- 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)
- O mapeamento para o modelo de dominio chamado na classe de implementação do repositório (
- Mapper na camada de apresentação:
- O mapeamento para o modelo de de UI é feito no
ViewModel
- O mapeamento para o modelo de de UI é feito no
- 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
ExampleToDomainMapperque poderá extender deMapper.
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) } }
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.
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 é umMutableLiveDataque 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
- 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
ViewModelquanto naActivity- 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
- Deve ser criada uma classe
Paramspara representar o modelo de parametros que será passada no construtor e na funçãorun - Esta
data classdeverá estar localizada dentro da classe do caso de usofun ExampleUseCase(exampleRepository: ExampleRepository): BaseUseCase<ResultWrapper<..>, ExampleUseCase.Params> { fun runSync(params: ExampleUseCase.Params): ResultWrapper<..> { } data class Params ( val exampleVariable: Int ) }
- 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 Exampleao invés denamed_param = get()devido a erro de compilação vs erro de execuçãofactory { ExampleUseCase(get() as ExampleRepository) }
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)
- 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
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.
Sim
- 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
- Para a troca de activities, será necessário criar um
companion objectcom um métodoopenActivity(<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)
}
}
}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
As constantes de extras ficarão dentro do companion object, conforme a CurrentActivity1 do tópico "passagem de parâmetros entre activities".
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.
- 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>
- Se for uma imagem, ícone, vector ou shape específico de uma determindada tela, utilizar o padrão
- Para ids:
-
<onde é>_<tipo de view>_<o que é>. Exemplo:paywall_imageview_close
-
Temos que realizar análise caso a caso, porém temos sempre como objetivo remover todos os warnings.
- presentation:
ExampleUIModel - domain:
ExampleModel - data source:
ExamplePayloadModelpara modelos de envio de dados eExampleResponseModelpara modelos de resposta. - banco de dados:
ExampleEntity(ainda será revisitado)
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(...) } }
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"
}