Adaptive Layout é uma abordagem de layout de interface onde o aplicativo usa layouts diferentes para diferentes tipos de dispositivos e orientações de tela. Ao contrário do responsive design, o adaptive layout alterna entre layouts pré-definidos em pontos de breakpoint. Este artigo aborda Size Classes e UITraitCollection no iOS, qualificadores sw600dp/layout-w600dp no Android e fornece exemplos práticos.
Pontos principais
Adaptive Layout é uma estratégia de UI onde o desenvolvedor cria layouts separados para cada tipo de dispositivo e alterna entre eles com base nas características da tela. Ao contrário do responsive design, onde os elementos fluem suavemente, o adaptive layout usa alternâncias discretas: telefone — um layout, tablet — outro, desktop — um terceiro.
A Apple introduziu Size Classes no iOS 8 (2014) junto com aplicativos universais (binários que funcionam tanto no iPhone quanto no iPad). O Google apresentou o qualificador smallestWidth (sw
As diretrizes modernas da Apple HIG e do Google Material Design concordam que o adaptive layout é essencial para aplicativos que suportam mais de um tamanho de tela. O Google Play marca a falta de adaptação para tablets como uma desvantagem. A App Store não tem um requisito formal, mas os usuários de iPad esperam uma experiência nativa, não um aplicativo de iPhone esticado.
Size Classes são uma característica do iOS que define o espaço disponível como Compact (Compacto) ou Regular (Regular) em cada eixo. UITraitCollection é um sistema iOS para passar características do ambiente pela hierarquia de views: tamanho, escala da tela, capacidade force touch, gama de cores (claro/escuro), configurações de acessibilidade.
| Dispositivo / Orientação | Horizontal | Vertical |
|---|---|---|
| iPhone retrato (todos os modelos) | Compact | Regular |
| iPhone Plus/Pro Max paisagem | Regular | Compact |
| iPhone SE / mini paisagem | Compact | Compact |
| iPad retrato | Regular | Regular |
| iPad paisagem | Regular | Regular |
| iPad Split View (1/3 da tela) | Compact | Regular |
O UITraitCollection é formado pelo sistema e passado do UIApplication através do UIWindow para cada UIView. Quando a orientação ou o tamanho da janela (Split View) muda, o sistema gera um novo UITraitCollection e chama traitCollectionDidChange. No SwiftUI, as mudanças são rastreadas através de Environment Values e a view é redesenhada automaticamente.
A adaptação via UITraitCollection no UIKit é implementada inscrevendo-se nas mudanças de trait collection e reconstruindo o layout. O exemplo abaixo mostra um controlador que alterna entre uma pilha vertical para iPhone e um layout dividido para iPad.
// Controlador adaptativo com Size Classes
import UIKit
final class AdaptiveViewController: UIViewController {
private let compactStack = UIStackView()
private let regularStack = UIStackView()
private let sidebar = UIView()
private let content = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateLayout(for: traitCollection)
}
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.horizontalSizeClass
!= previousTraitCollection?.horizontalSizeClass {
updateLayout(for: traitCollection)
}
}
private func updateLayout(for traits: UITraitCollection) {
if traits.horizontalSizeClass == .regular {
showRegularLayout() // iPad: barra lateral + conteúdo
} else {
showCompactLayout() // iPhone: barra de abas + push
}
}
private func showRegularLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
regularStack.addArrangedSubview(sidebar)
regularStack.addArrangedSubview(content)
regularStack.frame = view.bounds
regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(regularStack)
}
private func showCompactLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
compactStack.addArrangedSubview(content)
compactStack.frame = view.bounds
compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(compactStack)
}
private func setupViews() {
sidebar.backgroundColor = .systemGray6
content.backgroundColor = .systemBackground
compactStack.axis = .vertical
regularStack.axis = .horizontal
}
}O método traitCollectionDidChange é chamado em qualquer mudança de traits, mas verificamos apenas horizontalSizeClass — o indicador mais significativo para troca de layout. No iOS 17+ é recomendado usar UIViewController.horizontalSizeClass como uma propriedade computada para atualizações reativas sem inscrição em mudanças.
O Android fornece dois qualificadores principais de largura: sw
| Qualificador | Condição | Exemplo de dispositivo |
|---|---|---|
| layout-sw600dp | Largura mínima ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Largura atual ≥ 600 dp | iPad em Split View (600 dp), telefone em paisagem |
| layout-land | Orientação landscape | Qualquer dispositivo girado horizontalmente |
| layout-sw720dp | Largura mínima ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Hierarquia de recursos no Android: res/layout/activity_main.xml (telefone), res/layout-sw600dp/activity_main.xml (tablet 7"), res/layout-w600dp-land/activity_main.xml (telefone em paisagem). O Android seleciona o qualificador mais específico que corresponde à configuração atual do dispositivo. Quando nenhum arquivo correspondente existe, o recurso base de res/layout/ é usado.
O desenvolvimento moderno Android usa Jetpack WindowManager para determinar o tamanho da janela em vez de ler recursos diretamente. Isso é especialmente importante para dispositivos dobráveis onde o tamanho da tela muda dinamicamente (Galaxy Fold se desdobra de 6.2" para 7.6").
// Adaptação via Jetpack WindowManager
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
FlowRow(
verticalAlignment = Arrangement.Top,
horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
else -> Arrangement.Start
}
) {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
CompactContent()
}
WindowWidthSizeClass.MEDIUM -> {
MediumContent()
}
WindowWidthSizeClass.EXPANDED -> {
ExpandedContent()
}
}
}
}
@Composable
private fun CompactContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text("Telefone", style = MaterialTheme.typography.headlineSmall)
ListContent()
}
}
@Composable
private fun MediumContent() {
Row(modifier = Modifier.padding(24.dp)) {
NavigationRail { ... }
Column { ListContent() }
}
}
@Composable
private fun ExpandedContent() {
Row(modifier = Modifier.padding(32.dp)) {
PermanentNavigationDrawer { ... }
Column {
ListContent()
}
DetailPane()
}
}O componente currentWindowAdaptiveInfo() da biblioteca androidx.window adapta a interface para qualquer dispositivo: telefone, tablet, dobrável, ChromeOS. Classes de largura: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Classes de altura: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI fornece ferramentas integradas para layout adaptativo sem usar diretamente Size Classes. AnyLayout, ViewThatFits e GeometryReader permitem construir interfaces que se ajustam automaticamente ao espaço disponível.
// Adaptação SwiftUI via AnyLayout e ViewThatFits
import SwiftUI
struct AdaptiveGrid: View {
@Environment(\.horizontalSizeClass) private var hSizeClass
var body: some View {
let layout = hSizeClass == .regular
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Label("Favoritos", systemImage: "star")
Label("Recentes", systemImage: "clock")
Label("Configurações", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — seleção automática de layout
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Prioridade 1: horizontal
CardView()
CardView()
}
VStack { // Se não couber — vertical
CardView()
CardView()
}
}
}
}ViewThatFits é uma ferramenta poderosa do SwiftUI que seleciona automaticamente a primeira view filha que cabe no espaço disponível sem cortes. Isso elimina a necessidade de verificar explicitamente Size Classes para alternâncias adaptativas simples. AnyLayout permite alternar o tipo de layout (HStack/VStack) sem if-else no corpo da view.
Jetpack Compose usa WindowSizeClass (da biblioteca Material 3 adaptive) e BoxWithConstraints para layout adaptativo. Ao contrário dos qualificadores XML, o Compose determina o tamanho da janela em tempo de execução e recompõe a UI reativamente em mudanças de configuração.
// Jetpack Compose adaptativo via BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tablet: duas colunas (list-detail)
Row(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(items) { item ->
ListItem(text = item)
}
}
var selected by remember { mutableStateOf(items.first()) }
DetailPanel(item = selected)
.weight(2f)
}
} else {
// Telefone: uma coluna com navegação
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
ListItem(text = item, onClick = { navigateToDetail(item) })
}
}
}
}
}
@Composable
fun AdaptiveListMaterial3() {
val windowClass = currentWindowAdaptiveInfo().windowSizeClass
AdaptiveLayout(
layout = windowClass.windowWidthSizeClass
) {
when (windowClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
else -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
}
}
}Material 3 Adaptive Layouts fornece componentes prontos: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Esses componentes se adaptam automaticamente ao WindowWidthSizeClass — alternando entre empilhado (COMPACT) e lado a lado (MEDIUM/EXPANDED). O desenvolvedor só precisa escolher um padrão e passar os painéis de conteúdo.
Perguntas frequentes
sw600dp (smallestWidth) — largura mínima da tela independentemente da orientação. Ativa sempre em tablets de 7"+. w600dp (available width) — largura disponível considerando a orientação atual. Ativa em telefones em paisagem onde a largura pode exceder 600 dp.
UITraitCollection é um objeto iOS que contém características do ambiente: size classes (horizontal/vertical), escala de exibição, capacidade force touch, idiom de interface do usuário (iPhone/iPad), gama de cores, configurações de acessibilidade. É passado do UIApplication pela hierarquia de views e muda quando a orientação ou o tamanho da janela muda.
Use Jetpack WindowManager (Android) e UIScreen nativeBounds + traitCollection (iOS). Dispositivos dobráveis mudam Size Class ao desdobrar. Recomenda-se uma arquitetura baseada em layouts canônicos (list-detail, supporting pane) com inscrição reativa a mudanças de tamanho de janela sem reiniciar a Activity.
Não. Use um único storyboard com variações de constraints por Size Class ou layout programático via UIKit. SwiftUI não requer storyboards. Storyboards separados para iPad criam duplicação e complicam a manutenção — é melhor adaptar um único layout através de Size Classes.
Canonical layouts são padrões de layout adaptativo prontos do Google: list-detail (lista + detalhe), supporting pane (conteúdo + painel de ações), feed (cartões). Cada padrão suporta três estados (compact/medium/expanded) e está implementado na biblioteca Material 3 Adaptive através de ListDetailScaffold e SupportingPaneScaffold.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também