Иногда в разработке случается баг, который не просто не даёт спать, а заставляет пересмотреть свои жизненные ценности.

У меня это произошло со скромным всплывающим окном внутри WebView.

Задача была максимально скучной: на экране с WebView пользователь нажимает кнопку “Exit”, а веб-страница показывает попап подтверждения. На старом коде — всё идеально. На браузере — идеально. На новом инфраструктурном слое WebView — попап появляется на миг и тут же закрывается сам, как будто кто-то тайно играет в “крестики” за пользователя.

Никаких ошибок. Никаких исключений.

Просто “блип”… и пустота.

То самое чувство, когда ты нажимаешь кнопку, а мир делает вид, что “ничего не было”.

И это — пролог к истории о том, как два дня моей жизни сгорели на алтаре WebView.

Немного контекста

Экран устроен просто: сверху нативный контейнер, внутри WebView, загружающий веб-витрину.

В вебе есть кнопка “Exit”.

Она открывает попап “Точно выйти?”.

Пользователь выбирает — всё счастливы.

На старой реализации (через Accompanist WebView) всё работало.

Но проект рос, экранов с WebView становилось больше, и мы решили сделать нормальный инфраструктурный слой:

  • единый контейнер

  • пререндер WebView

  • реюз экземпляров

  • метрики — время до первого кадра и т.д

  • аккуратные JS-бриджи;

  • предсказуемый жизненный цикл.

Сверху всё выглядело более чем прилично.

Пока мы не нажали ту самую кнопку “Exit”.

Симптом: попап живёт меньше секунды

Сценарий:

  • старая реализация → попап висит, можно нажимать

  • новая реализация → попап мелькает и исчезает сам.

Без крэшей. Без ошибок.

Ни тебе stacktrace, ни даже “undefined is not a function”.

Тот самый случай, когда ощущаешь себя не разрабом, а шаманом с бубном.

Подозрение №1: cookies — стандартный злодей


Первое, что приходит в голову: конечно же, куки.

Ну кто ещё?

Если где-то пропала авторизация — веб обычно начинает творить странные вещи.

Проверили всё:

  • куки на месте

  • домены совпадают

  • значения совпадают

  • WebView их видит

  • браузер видит

  • старая версия видит

Куки невиновны.

Подозрение №2: JS-мосты


Если не куки — значит, сломали что-то в порядке навешивания бриджей:

  • JSBridge

  • NativeBridge

  • WebViewClient

  • ChromeClient

  • детектор рендера

Мы проверили:

  1. все интерфейсы доступны

  2. сообщения приходят

  3. в консоли нет ошибок

  4. порядок навешивания корректный

JS-мост жив-здоров -- Баг тоже)

Подозрение №3: “Это точно пререндер”

Хорошо. Выключаем пререндер.

Оставляем максимально простой сценарий:

  • new WebView

  • loadUrl

  • никаких кешей

  • никаких магических оптимизаций.

Попап всё равно умирает, как будто у него срок годности истёк.

Когда нативные логи уже не помогают


Мы начали логировать всё: размеры, прикрепление к окну, моменты loadUrl, настройки, cookie, debug-информацию.

Логи выглядели примерно так:

POST: size=1080x2165, attached=true
POST: loadUrl(…)

То есть WebView успевает встроиться в дерево, получает размер, и только потом начинаем загрузку.

Всё корректно.

С точки зрения Android — вообще идеальная картинка.

Но попап всё равно продолжает исчезать.

И тут мы попросили web-разработку включить свои логи


Это был поворотный момент.

На веб-стороне мы включили логирование компонента, который отвечает за попап.

И увидели следующее:

ContextMenu rendering (hidden=false)

ContextMenu mounted

ContextMenu hidden state changed (hidden=false)

ContextMenu rendering…

ContextMenu rendering placement=‘top’

ContextMenu rendering placement=‘top’

ContextMenu is out of viewport → closing

ContextMenu handleClose

popup closed

Вот оно. Фронтенд САМ закрывает попап, потому что считает, что он…

вылетел за пределы вьюпорта.

Почему старый WebView считался “вьюпортно правильным”, а новый — нет?


Фронтенд обычно проверяет попадание элемента в экран через getBoundingClientRect и window.innerHeight.

И вот тут — магия WebView:

  • нативный контейнер стал другим

  • изменились Insets

  • состав обёрток вокруг WebView сменился

  • высота вычисляется чуть иначе

  • браузерный layout внутри WebView получает другую “геометрию”

Разница может быть даже в 1–2 пикселя.

Но если логика попапа строгая — она решает “элемент частично вне экрана” → закрыть.

Что с этим делать

Варианты фиксов на стороне веба:

  1. Сделать проверку менее строгой.

    Например, разрешить на 10–20 px вылет, если это безопасно.

  2. Добавить fallback-режим для WebView.

    Если User-Agent содержит android-webview — использовать другой алгоритм вычисления.

  3. Отключить проверку “вьюпорта” для критичных попапов.

  4. Использовать другой метод позиционирования (не getBoundingClientRect).

Финал

Этот баг стал отличным напоминанием:

WebView — это не “браузер внутри приложения”,

а два разных мира, которые живут рядом и каждый со своими правилами.

Если ваша команда когда-нибудь поймает похожий баг — надеюсь, эта история сэкономит вам пару суток и немного нервных клеток.

Комментарии (3)