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
0dpem 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.MaterialToolbarcom.google.android.material.button.MaterialButtoncom.google.android.material.textfield.TextInputLayout+TextInputEditTextcom.google.android.material.card.MaterialCardViewcom.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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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 comum | Prática recomendada |
|---|---|
| Layout quebra em telas pequenas | Use scroll para conteúdo variável e evite alturas fixas desnecessárias |
| Elementos desalinhados | Use constraints consistentes, margens padrão e chains quando houver múltiplos botões |
| Feedback insuficiente | Use Snackbar para ações rápidas e dialogs para confirmações importantes |
| Repetição de XML | Extraia para <include>, estilos e layouts reutilizáveis |
| Estado vazio tratado como erro | Separe claramente: vazio (sucesso sem dados) vs erro (falha) |