Тестирование совместимости мобильного приложения на разных устройствах
Дизайнер нарисовал макет под iPhone 14 Pro с экраном 6.1'' и Dynamic Island. Разработчик проверял на том же устройстве. Релиз вышел, и выяснилось: на Samsung Galaxy A03 с экраном 720×1600 кнопка «Подтвердить» уходит за пределы экрана, потому что верстали под safe area, которого на старых Samsung нет. На iPad Mini нижняя навигация занимает треть экрана, потому что никто не тестировал на планшете.
Устройства слишком разные. Единого решения нет — есть методичное тестирование.
Чем различаются устройства
Не только размером экрана. Вот что реально влияет на поведение приложения:
Разрешение и плотность пикселей. ldpi (120 dpi), mdpi (160), hdpi (240), xhdpi (320), xxhdpi (480), xxxhdpi (640). Иконки без адаптированных вариантов под плотность выглядят размыто или огромными. На Redmi с 720p и Samsung с 1080p при той же физической диагонали — разная плотность, разные dp→px пересчёты.
Соотношение сторон. Стандарт 16:9 (720×1280) устарел. Актуальные: 20:9 (Samsung S-серия), 19.5:9 (iPhone), 21:9 (Sony Xperia), складные устройства с переменным соотношением. Верстка, хардкодящая высоту элементов, ломается на нестандартных пропорциях.
Safe Area и вырезы. Dynamic Island (iPhone 14 Pro+), notch-hole (Samsung), punch-hole camera (большинство современных Android). На Android — WindowInsets и displayCutout. Без обработки вырезов контент уходит под камеру.
Производительность железа. Qualcomm Snapdragon 8 Gen 3 в флагмане vs MediaTek Helio G85 в бюджетнике — это разные уровни производительности. Анимации на 120 fps, которые плавно работают на флагмане, дёргаются на mid-range. Сложные Compose-лейауты с multiple recompositions это почувствуют.
Аппаратные возможности. Нет NFC (бюджетные устройства), нет барометра, нет LiDAR, нет Face ID. Без проверки PackageManager.hasSystemFeature() попытка использовать недоступное железо — крэш или silent fail.
Матрица устройств
Составляем на основе аналитики (Firebase, Mixpanel по device_model), общей статистики рынка и бизнес-требований:
| Категория | Примеры | Почему включаем |
|---|---|---|
| Флагман iOS | iPhone 15 Pro, iPhone 14 | Целевая аудитория, Dynamic Island |
| Компактный iOS | iPhone SE 3rd gen | Маленький экран, нет notch |
| Планшет iOS | iPad (10th gen), iPad Pro | Широкий экран, Split View |
| Флагман Android | Google Pixel 8, Samsung S24 | Актуальная Android, OLED |
| Mid-range Android | Samsung A54, Xiaomi Redmi Note 12 | Крупнейшая доля рынка |
| Бюджетный Android | Samsung A03, Redmi 10 | Низкая производительность, 720p |
| Планшет Android | Samsung Galaxy Tab S9 | Адаптивный layout |
| Складной | Samsung Z Fold 5 | Если поддерживается форм-фактор |
Минимальная матрица для большинства проектов: 5–7 устройств. Больше — через BrowserStack или Firebase Test Lab (реальные устройства без покупки).
Тестирование адаптивного layout
На Android — WindowSizeClass из Jetpack Compose:
val windowSizeClass = calculateWindowSizeClass(this)
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> PhoneLayout() // < 600dp
WindowWidthSizeClass.Medium -> TabletLayout() // 600–840dp
WindowWidthSizeClass.Expanded -> DesktopLayout() // > 840dp
}
Тестируем все три класса: Compact (телефон portrait), Medium (планшет portrait или телефон landscape), Expanded (планшет landscape).
На iOS — horizontalSizeClass в SwiftUI:
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .compact {
VStack { ... }
} else {
HStack { ... } // iPad, landscape iPhone Plus
}
}
Специфика складных устройств
Samsung Galaxy Z Fold — два режима: сложен (compact, 22:9) и раскрыт (большой экран, ~4:3). Приложение должно корректно переходить между режимами без потери состояния. onConfigurationChanged вызывается при раскрытии/складывании. Если Activity пересоздаётся — все несохранённые данные в форме теряются.
Проверяем: фокус в TextField сохраняется, скролл-позиция восстанавливается, модальные окна не «прыгают».
Проверка аппаратных возможностей
// Android: проверяем перед использованием
val hasBluetooth = packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
val hasNfc = packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
val hasCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
Если функция недоступна — скрываем UI-элемент или показываем объяснение. Не крэшимся, не показываем недоступную кнопку.
Сроки
2–3 дня — составление матрицы устройств по аналитике, тестирование на приоритетных устройствах (эмуляторы + облачная ферма), отчёт с матрицей несовместимостей и скриншотами. Стоимость рассчитывается индивидуально.







