Recomendações para a arquitetura do Android

Nesta página, apresentamos várias práticas recomendadas e sugestões para a Arquitetura. Adote nossas dicas para melhorar a qualidade, robustez e escalonabilidade do app. Elas também facilitam os processos de manutenção e teste.

As práticas recomendadas abaixo estão agrupadas por assunto. Um nível de importância foi atribuído a cada uma delas, de acordo com a recomendação da nossa equipe. Os níveis são os seguintes:

  • Altamente recomendável:essa prática deveria ser implementada em qualquer caso que não entre em conflito direto com a sua abordagem.
  • Recomendável: essa prática provavelmente vai melhorar seu app.
  • Opcional: essa prática pode melhorar seu app em algumas circunstâncias.

Arquitetura com camadas

A arquitetura com camadas recomendada favorece a separação de conceitos. Ela baseia a IU em modelos de dados, segue os princípios de fonte única de verdade e de fluxo de dados unidirecional. Confira algumas práticas recomendadas para arquitetura com camadas:

Recomendação Descrição
Usar uma camada de dados claramente definida. A camada de dados expõe os dados do aplicativo ao restante do app e contém a grande maioria das lógicas de negócios dele.
  • Crie repositórios, mesmo que eles contenham somente uma fonte de dados.
  • Em apps pequenos, é possível colocar tipos de camada de dados em um pacote ou módulo de data.
Usar uma camada de IU claramente definida. A camada de IU mostra os dados do app na tela e atua como o ponto principal de interação do usuário.
  • Em apps pequenos, é possível colocar tipos de camada de dados em um pacote ou módulo de ui.
Confira mais práticas recomendadas para camadas de IU.
A camada de dados precisa expor os dados do app usando um repositório.

Componentes na camada da interface, como elementos combináveis, atividades ou ViewModels, não podem interagir diretamente com uma fonte de dados. Confira alguns exemplos de fontes de dados:

  • Bancos de dados, DataStore, SharedPreferences, APIs do Firebase.
  • Provedores de localização de GPS.
  • Provedores de dados Bluetooth.
  • Provedor de status da conectividade de rede.
Usar corrotinas e fluxos. Usar corrotinas e fluxos para estabelecer a comunicação entre as camadas.

Confira mais práticas recomendadas para corrotinas.

Usar uma camada de domínios. Use uma camada de domínios se precisar reutilizar a lógica de negócios que interage com a camada de dados em vários ViewModels ou se quiser simplificar a complexidade da lógica de negócios de um ViewModel específico.

Camada de IU

A função da camada de IU é mostrar os dados do app na tela e atuar como o ponto principal de interação do usuário. Confira algumas práticas recomendadas para a camada de IU:

Recomendação Descrição
Seguir o fluxo de dados unidirecional (UDF, na sigla em inglês). Siga os princípios do fluxo de dados unidirecional (UDF), em que os ViewModels expõem o estado da interface usando o padrão de observador e recebem ações da interface por chamadas de método.
Usar os ViewModels do AAC, caso sejam vantajosos para o app. Use ViewModels do AAC para processar a lógica de negócios e busque dados do app para expor o estado da IU à IU do app (no Compose ou nas visualizações do Android).

Confira mais práticas recomendadas do ViewModel.

Consulte os benefícios dos ViewModels.

Usar a coleta de estado da IU com reconhecimento de ciclo de vida. Colete o estado da interface usando o builder adequado de corrotinas com reconhecimento de ciclo de vida: repeatOnLifecycle no sistema de visualização do Android e collectAsStateWithLifecycle no Jetpack Compose.

Leia mais sobre repeatOnLifecycle (link em inglês).

Saiba mais sobre o collectAsStateWithLifecycle (em inglês).

Não enviar eventos do ViewModel para a IU. Processe o evento imediatamente no ViewModel e cause uma atualização de estado com o resultado. Saiba mais sobre os eventos de IU.
Usar um aplicativo de atividade única. Use fragmentos ou componentes de navegação para alternar entre as diferentes telas e links diretos para o app, caso ele tenha mais de uma tela.
Usar o Jetpack Compose. Use o Jetpack Compose para criar novos apps para smartphones, tablets, dispositivos dobráveis e para Wear OS.

O snippet abaixo descreve como coletar o estado da IU de uma forma que reconhece o ciclo de vida:

Visualizações

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

Compose

@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
}

ViewModel

Os ViewModels são responsáveis por fornecer o estado da IU e o acesso à camada de dados. Confira algumas práticas recomendadas para ViewModels:

Recomendação Descrição
Os ViewModels precisam ser independentes do ciclo de vida do Android. Os ViewModels não podem conter uma referência a nenhum tipo relacionado ao ciclo de vida. Não transmita Activity, Fragment, Context ou Resources como uma dependência. Caso um Context seja necessário no ViewModel, analise se esse elemento está na camada certa.
Usar corrotinas e fluxos.

O ViewModel interage com as camadas de dados ou de domínio usando:

  • Fluxos do Kotlin para receber dados do app.
  • Funções suspend para executar ações com viewModelScope.
Usar ViewModels na tela.

Não use ViewModels em partes reutilizáveis da IU. Em vez disso, use-os em:

  • Elementos combináveis da tela.
  • Atividades ou fragmentos nas visualizações.
  • Destinos ou gráficos ao usar a navegação do Jetpack.
Use classes de detentores de estado simples em componentes de IU reutilizáveis. Use classes de detentores de estado simples para processar a complexidade em componentes de IU reutilizáveis para que o estado possa ser elevado e controlado externamente.
Não use o AndroidViewModel. Use a classe ViewModel, e não AndroidViewModel. A classe Application não pode ser usada no ViewModel. Em vez disso, mova a dependência para a IU ou para a camada de dados.
Expor um estado da IU. Os ViewModels precisam expor dados à IU usando uma única propriedade conhecida como uiState. Se a IU mostrar diferentes dados não relacionados, a VM pode expor várias propriedades do estado da IU.
  • Transforme uiState em StateFlow.
  • Crie o uiState usando o operador stateIn com a política WhileSubscribed(5000), por exemplo, (links em inglês) se os dados forem originados de outras camadas da hierarquia como um fluxo de dados.
  • Para casos mais simples sem fluxos de dados provenientes da camada de dados, é aceitável usar um MutableStateFlow exposto como um StateFlow imutável (exemplo em inglês).
  • É possível optar por usar ${Screen}UiState como uma classe de dados que pode conter dados, erros e sinais de carregamento. Ela também pode ser uma classe selada, desde que os diferentes estados sejam exclusivos.

O snippet abaixo demonstra como expor o estado da IU de um ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

Ciclo de vida

Apresentamos abaixo algumas práticas recomendadas para trabalhar com o ciclo de vida do Android:

Recomendação Descrição
Não substitua métodos de ciclo de vida em atividades ou fragmentos. Não substitua os métodos de ciclo de vida, como onResume, em atividades ou fragmentos. Em vez disso, use um LifecycleObserver. Caso o app precise executar o trabalho quando o ciclo de vida atingir um determinado Lifecycle.State, use a API repeatOnLifecycle.

O snippet abaixo descreve como executar operações de acordo com um determinado estado do ciclo de vida:

Visualizações

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

Compose

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

Gerenciar dependências

Existem várias práticas recomendadas que precisam ser consideradas ao gerenciar dependências entre componentes:

Recomendação Descrição
Usar a injeção de dependência. Quando possível, use as práticas recomendadas de injeção de dependência, principalmente a injeção de construtor.
Definir um componente como escopo quando necessário. Defina um contêiner de dependência como escopo caso o tipo contenha dados mutáveis que precisem ser compartilhados ou caso ele demande muita memória e bateria para inicializar e seja amplamente usado no app.
Usar o Hilt. Use o Hilt ou a injeção manual de dependência em apps simples. Use o Hilt caso se trate de um projeto complexo. Por exemplo, se ele incluir:
  • Várias telas com ViewModels (integração).
  • Uso do WorkManager (integração).
  • Uso avançado da navegação, como ViewModels com um gráfico de navegação como escopo (integração).

Testes

Confira algumas práticas recomendadas para testes:

Recomendação Descrição
Saiba o que testar.

A menos que seu projeto seja quase tão simples quanto um app Hello World, você precisa testá-lo usando, pelo menos:

  • ViewModels de teste de unidade, incluindo fluxos.
  • Entidades da camada de dados de teste de unidade, ou seja, repositórios e fontes de dados.
  • Testes de navegação da IU, que são úteis como testes de regressão na CI.
Usar falsificações em vez de simulações. Saiba mais na documentação Uso de testes duplos no Android.
Testar StateFlows. Ao testar StateFlow:

Para mais informações, consulte o guia O que testar no Android.

Modelos

Considere as práticas recomendadas abaixo ao desenvolver modelos nos seus apps:

Recomendação Descrição
Criar um modelo por camada em apps complexos.

Em apps complexos, crie novos modelos em camadas ou componentes diferentes quando apropriado. Confira estes exemplos:

  • Uma fonte de dados remota pode transformar o modelo recebido pela rede em uma classe mais simples, contendo apenas os dados de que o app precisa.
  • Os repositórios podem transformar modelos DAO em classes de dados mais simples, contendo apenas as informações de que a camada de interface precisa.
  • O ViewModel pode incluir modelos de camada de dados em classes UiState.

Convenções de nomenclatura

Ao definir a nomenclatura da base de código, é necessário considerar estas práticas recomendadas:

Recomendação Descrição
Nomear métodos.
Opcional
Use frases verbais para nomear os métodos. Por exemplo, makePayment().
Nomear propriedades.
Opcional
Use frases substantivas para nomear propriedades. Por exemplo, inProgressTopicSelection.
Nomear fluxos de dados.
Opcional
Quando uma classe expõe um fluxo de dados, LiveData ou qualquer outro fluxo, a convenção de nomenclatura é usar get{model}Stream(). Por exemplo, getAuthorStream(): Flow<Author>. Se a função retornar uma lista de modelos, o nome do modelo precisa estar no plural: getAuthorsStream(): Flow<List<Author>>.
Nomear implementações de interfaces.
Opcional
Os nomes das implementações de interfaces precisam representar o que elas significam. Defina Default como prefixo, se não encontrar um nome mais adequado. Por exemplo, para uma interface NewsRepository, é possível usar OfflineFirstNewsRepository ou InMemoryNewsRepository. Se não encontrar um nome melhor, use DefaultNewsRepository. Implementações falsas precisam incluir o prefixo Fake, como FakeAuthorsRepository.