Interface Android moderna: layouts, componentes e boas práticas de UI

Capítulo 3

Tempo estimado de leitura: 18 minutos

+ Exercício

Layouts modernos: pensando em hierarquia, constraints e responsividade

Uma UI Android moderna precisa ser: previsível (o usuário entende o que acontece), responsiva (adapta-se a tamanhos/orientações), consistente (estilos e componentes coerentes) e eficiente (hierarquia de views enxuta). Neste capítulo você vai construir telas usando ConstraintLayout e componentes Material, além de aplicar padrões de reutilização e estados de tela.

Hierarquia de views: por que importa

Quanto mais profunda e complexa a hierarquia, maior o custo de medida/layout/desenho. A prática recomendada é: preferir um layout “pai” poderoso (como ConstraintLayout) e evitar aninhamentos desnecessários.

  • Evite: LinearLayout dentro de LinearLayout dentro de ScrollView sem necessidade.
  • Prefira: ConstraintLayout como container principal e, quando necessário, grupos simples (ex.: um LinearLayout horizontal pequeno).
  • Use 0dp em largura/altura no ConstraintLayout para “match constraints” (preencher o espaço entre constraints).

ConstraintLayout na prática: regras essenciais

  • Todo elemento precisa de constraints (horizontal e vertical) para ter posição definida.
  • Chains alinham e distribuem elementos (útil para botões lado a lado).
  • Guidelines ajudam a manter margens proporcionais (ex.: 16% da largura).
  • Barriers permitem posicionar views com base no tamanho dinâmico de outras.

Componentes Material: consistência visual e acessibilidade

Use componentes do Material para obter comportamento e aparência consistentes: campos com erro, botões com estados, feedback com Snackbar, etc. Em layouts XML, prefira as versões Material:

  • com.google.android.material.appbar.MaterialToolbar
  • com.google.android.material.button.MaterialButton
  • com.google.android.material.textfield.TextInputLayout + TextInputEditText
  • com.google.android.material.card.MaterialCardView
  • com.google.android.material.progressindicator.CircularProgressIndicator

Exemplo: campo de texto com validação visual

O padrão recomendado é envolver o input em um TextInputLayout, pois ele gerencia hint, erro e ícones.

<com.google.android.material.textfield.TextInputLayout android:id="@+id/tilName" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="Nome" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_margin="16dp"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/etName" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textPersonName"/></com.google.android.material.textfield.TextInputLayout>

No Kotlin, você pode ativar o erro quando o texto estiver inválido:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

val name = binding.etName.text?.toString().orEmpty().trim() if (name.isEmpty()) {     binding.tilName.error = "Informe um nome" } else {     binding.tilName.error = null }

Suporte a diferentes tamanhos de tela: estratégias práticas

1) Use dimensões e espaçamentos consistentes

Evite “números mágicos” repetidos. Centralize espaçamentos em recursos quando fizer sentido (ex.: dimens.xml) e use margens padrão (8dp, 16dp, 24dp).

2) Scroll quando o conteúdo pode crescer

Para formulários e telas com conteúdo variável, use NestedScrollView para evitar cortes em telas pequenas.

<androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> ... </androidx.constraintlayout.widget.ConstraintLayout></androidx.core.widget.NestedScrollView>

3) Recursos alternativos por qualificador

Para adaptar layouts e dimensões, use pastas de recursos com qualificadores:

  • layout/ (padrão)
  • layout-land/ (paisagem)
  • layout-sw600dp/ (tablets/maiores)
  • values/, values-sw600dp/ para dimensões e estilos

Exemplo: em values/dimens.xml e values-sw600dp/dimens.xml, defina um padding maior para telas grandes, sem mudar o código.

Telas com estados: carregando, conteúdo, vazio e erro

Uma tela real raramente é “só conteúdo”. Ela alterna entre estados: carregando (busca), conteúdo (sucesso), vazio (sem dados) e erro (falha). Implementar isso melhora a experiência e reduz bugs de UI.

Abordagem simples com containers e visibilidade

Você pode criar quatro containers e alternar visibilidade. Para evitar repetição, crie uma função que recebe o estado e ajusta a UI.

enum class UiState { LOADING, CONTENT, EMPTY, ERROR }

Layout exemplo (containers):

<androidx.constraintlayout.widget.ConstraintLayout ...> <com.google.android.material.progressindicator.CircularProgressIndicator android:id="@+id/viewLoading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/viewContent" android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> ... </androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout android:id="@+id/viewEmpty" android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" android:visibility="gone" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Nada por aqui"/> </LinearLayout> <LinearLayout android:id="@+id/viewError" android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" android:visibility="gone" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Ocorreu um erro"/> <com.google.android.material.button.MaterialButton android:id="@+id/btnRetry" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Tentar novamente"/> </LinearLayout></androidx.constraintlayout.widget.ConstraintLayout>

Função para alternar estados:

private fun render(state: UiState) = with(binding) {     viewLoading.visibility = if (state == UiState.LOADING) View.VISIBLE else View.GONE     viewContent.visibility = if (state == UiState.CONTENT) View.VISIBLE else View.GONE     viewEmpty.visibility = if (state == UiState.EMPTY) View.VISIBLE else View.GONE     viewError.visibility = if (state == UiState.ERROR) View.VISIBLE else View.GONE }

Boas práticas para estados

  • Não deixe a tela “piscando”: ao iniciar, mostre loading rapidamente e só troque para conteúdo quando tiver dados.
  • Estado vazio é diferente de erro: vazio é sucesso sem itens; erro é falha.
  • Botão de retry no erro reduz frustração.

Feedback ao usuário: Snackbar e dialogs

Snackbar: mensagens rápidas e ações

Use Snackbar para feedback não bloqueante (ex.: “Salvo” com ação “Desfazer”). Ele deve ser ancorado em uma view do layout (geralmente o root).

Snackbar.make(binding.root, "Item salvo", Snackbar.LENGTH_LONG)     .setAction("Desfazer") {         // reverter ação     }     .show()

Dialogs: confirmação e decisões importantes

Use dialog quando a ação exige confirmação (ex.: excluir). Prefira MaterialAlertDialogBuilder para manter o padrão visual.

MaterialAlertDialogBuilder(requireContext())     .setTitle("Excluir")     .setMessage("Deseja excluir este item?")     .setNegativeButton("Cancelar", null)     .setPositiveButton("Excluir") { _, _ ->         // executar exclusão     }     .show()

Reutilização: include, estilos e componentes reaproveitáveis

<include>: reaproveitando partes de layout

Quando um bloco se repete (ex.: um cabeçalho, um card padrão), extraia para um XML separado e inclua onde precisar.

<include layout="@layout/view_section_header" />

Se precisar ajustar constraints do include, você pode aplicar atributos no próprio <include>:

<include android:id="@+id/header" layout="@layout/view_section_header" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>

Estilos: padronizando aparência sem repetir atributos

Crie estilos para componentes recorrentes (ex.: títulos de seção, botões secundários). Assim você muda em um lugar e reflete no app todo.

<TextView style="@style/TextAppearance.App.SectionTitle" android:text="Destaques" />

Componentes reutilizáveis com MaterialCardView

Um padrão comum é criar um “card de item” reutilizável (imagem + título + subtítulo). Você pode criar um layout view_item_card.xml e incluí-lo em listas ou telas.

<com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp"> <TextView android:id="@+id/tvTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Título" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <TextView android:id="@+id/tvSubtitle" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Subtítulo" app:layout_constraintTop_toBottomOf="@id/tvTitle" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout></com.google.android.material.card.MaterialCardView>

Exercício prático 1: Tela inicial (Home) com estados e lista simples

Objetivo

Criar uma tela inicial com Toolbar, campo de busca, botão de ação e uma área central que alterna entre loading/conteúdo/vazio/erro. Você vai validar visualmente no emulador em diferentes tamanhos e orientação.

Passo 1: Estrutura do layout (ConstraintLayout + NestedScrollView)

Crie um arquivo fragment_home.xml (ou activity_home.xml, dependendo do seu projeto) com:

  • Toolbar no topo
  • Campo de busca (TextInputLayout)
  • Botão “Buscar”
  • Containers de estado
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="0dp" android:layout_height="wrap_content" android:title="Home" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <com.google.android.material.textfield.TextInputLayout android:id="@+id/tilQuery" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="Buscar" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/etQuery" android:layout_width="match_parent" android:layout_height="wrap_content" android:imeOptions="actionSearch" android:inputType="text"/> </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.button.MaterialButton android:id="@+id/btnSearch" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Buscar" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@id/tilQuery" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <FrameLayout android:id="@+id/stateContainer" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="@id/btnSearch" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <com.google.android.material.progressindicator.CircularProgressIndicator android:id="@+id/viewLoading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone"/> <androidx.core.widget.NestedScrollView android:id="@+id/viewContent" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:visibility="gone"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <include layout="@layout/view_item_card" /> <include layout="@layout/view_item_card" /> <include layout="@layout/view_item_card" /> </LinearLayout> </androidx.core.widget.NestedScrollView> <LinearLayout android:id="@+id/viewEmpty" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:visibility="gone"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Nenhum resultado"/> </LinearLayout> <LinearLayout android:id="@+id/viewError" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:visibility="gone"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Falha ao buscar"/> <com.google.android.material.button.MaterialButton android:id="@+id/btnRetry" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Tentar novamente"/> </LinearLayout> </FrameLayout></androidx.constraintlayout.widget.ConstraintLayout>

Passo 2: Lógica de estados e validação do campo de busca

Implemente:

  • Validação: não permitir busca vazia (erro no TextInputLayout)
  • Ao buscar: mostrar loading, depois alternar para conteúdo/vazio/erro (simulado)
  • Feedback: Snackbar ao iniciar busca e ao falhar
private fun validateQuery(): String? {     val q = binding.etQuery.text?.toString().orEmpty().trim()     return if (q.isEmpty()) {         binding.tilQuery.error = "Digite algo para buscar"         null     } else {         binding.tilQuery.error = null         q     } } private fun render(state: UiState) = with(binding) {     viewLoading.visibility = if (state == UiState.LOADING) View.VISIBLE else View.GONE     viewContent.visibility = if (state == UiState.CONTENT) View.VISIBLE else View.GONE     viewEmpty.visibility = if (state == UiState.EMPTY) View.VISIBLE else View.GONE     viewError.visibility = if (state == UiState.ERROR) View.VISIBLE else View.GONE } private fun setupClicks() {     binding.btnSearch.setOnClickListener {         val q = validateQuery() ?: return@setOnClickListener         Snackbar.make(binding.root, "Buscando: $q", Snackbar.LENGTH_SHORT).show()         render(UiState.LOADING)         binding.root.postDelayed({             val simulated = q.lowercase()             when {                 simulated.contains("erro") -> {                     render(UiState.ERROR)                     Snackbar.make(binding.root, "Não foi possível buscar", Snackbar.LENGTH_LONG).show()                 }                 simulated.contains("vazio") -> render(UiState.EMPTY)                 else -> render(UiState.CONTENT)             }         }, 900)     }     binding.btnRetry.setOnClickListener {         binding.btnSearch.performClick()     } }

Validação visual no emulador (checklist)

  • Teste em um dispositivo pequeno (ex.: 5") e um maior (ex.: 7" ou tablet).
  • Gire para paisagem e verifique se o conteúdo continua acessível (scroll).
  • Digite vazio e confirme se o erro aparece no campo.
  • Digite “vazio” para ver estado vazio; digite “erro” para ver estado de erro.

Exercício prático 2: Tela de detalhes com layout responsivo e confirmação

Objetivo

Criar uma tela de detalhes com imagem (placeholder), título, descrição, um botão primário e uma ação destrutiva com confirmação via dialog. Também incluir validação visual (ex.: quantidade mínima) e feedback com Snackbar.

Passo 1: Layout com ConstraintLayout e seções reutilizáveis

Crie fragment_details.xml com uma estrutura que funcione bem em telas pequenas usando scroll.

<androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="0dp" android:layout_height="wrap_content" android:title="Detalhes" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <com.google.android.material.card.MaterialCardView android:id="@+id/cardImage" android:layout_width="0dp" android:layout_height="180dp" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:contentDescription="Imagem do item" android:src="@android:color/darker_gray"/> </com.google.android.material.card.MaterialCardView> <TextView android:id="@+id/tvTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Título do item" android:textSize="20sp" android:textStyle="bold" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="@id/cardImage" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <TextView android:id="@+id/tvDescription" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Descrição longa do item..." android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@id/tvTitle" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <com.google.android.material.textfield.TextInputLayout android:id="@+id/tilQty" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="Quantidade" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="@id/tvDescription" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/etQty" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="number"/> </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.button.MaterialButton android:id="@+id/btnPrimary" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Adicionar" android:layout_marginTop="12dp" app:layout_constraintTop_toBottomOf="@id/tilQty" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <com.google.android.material.button.MaterialButton android:id="@+id/btnDelete" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Excluir" android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@id/btnPrimary" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout></androidx.core.widget.NestedScrollView>

Passo 2: Validação visual da quantidade e feedback

Regras sugeridas:

  • Quantidade obrigatória
  • Quantidade mínima 1
private fun validateQty(): Int? {     val raw = binding.etQty.text?.toString().orEmpty().trim()     val qty = raw.toIntOrNull()     return when {         raw.isEmpty() -> {             binding.tilQty.error = "Informe a quantidade"             null         }         qty == null -> {             binding.tilQty.error = "Valor inválido"             null         }         qty < 1 -> {             binding.tilQty.error = "Mínimo: 1"             null         }         else -> {             binding.tilQty.error = null             qty         }     } } private fun setupDetailsActions() {     binding.btnPrimary.setOnClickListener {         val qty = validateQty() ?: return@setOnClickListener         Snackbar.make(binding.root, "Adicionado: $qty", Snackbar.LENGTH_SHORT).show()     }     binding.btnDelete.setOnClickListener {         MaterialAlertDialogBuilder(requireContext())             .setTitle("Excluir item")             .setMessage("Essa ação não pode ser desfeita.")             .setNegativeButton("Cancelar", null)             .setPositiveButton("Excluir") { _, _ ->                 Snackbar.make(binding.root, "Item excluído", Snackbar.LENGTH_SHORT).show()             }             .show()     } }

Validação visual no emulador (checklist)

  • Digite vazio: deve aparecer erro no campo.
  • Digite 0: deve aparecer “Mínimo: 1”.
  • Digite 2: erro deve sumir e Snackbar deve aparecer ao tocar em “Adicionar”.
  • Toque em “Excluir”: dialog deve abrir; ao confirmar, mostrar Snackbar.
  • Teste em paisagem: verifique se tudo continua acessível com scroll.

Boas práticas rápidas de UI para aplicar nas suas telas

Problema comumPrática recomendada
Layout quebra em telas pequenasUse scroll para conteúdo variável e evite alturas fixas desnecessárias
Elementos desalinhadosUse constraints consistentes, margens padrão e chains quando houver múltiplos botões
Feedback insuficienteUse Snackbar para ações rápidas e dialogs para confirmações importantes
Repetição de XMLExtraia para <include>, estilos e layouts reutilizáveis
Estado vazio tratado como erroSepare claramente: vazio (sucesso sem dados) vs erro (falha)

Agora responda o exercício sobre o conteúdo:

Ao criar uma tela que alterna entre carregando, conteúdo, vazio e erro, qual abordagem reduz repetição e melhora a clareza do código de UI?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Separar a UI em containers (loading, content, empty, error) e alternar visibility via uma função baseada em um enum centraliza a lógica, evita repetição e reduz bugs de estado.

Próximo capitúlo

Recursos no Android: strings, cores, dimensões, temas e internacionalização

Arrow Right Icon
Capa do Ebook gratuito Android para Iniciantes com Kotlin: construindo seu primeiro app moderno
20%

Android para Iniciantes com Kotlin: construindo seu primeiro app moderno

Novo curso

15 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.