Найти в Дзене
Marco Hoffman

"Временные" и "постоянные" технологии в разработке софта. Пример из Android.

"В айти постоянно надо учиться..." - говорили они.

Расскажу-ка я на этот счёт одну историю из Android-разработки.

Давным давно, при появлении системы Android, Google сделал формат xml приоритетным способом задания вёрстки экранов, строк, ресурсов и много другого. Во многом это было продиктовано множеством экранов, языков и конфигураций системы: этим ресурсам требовалась "своя" организация. Отчасти это было связано с тогдашней "модой" на паттерн Model-View-Controller: в коде существовали классы -"контроллеры" экранов (Activity, Fragment) - и соответствующая вёрстка для них.
Для обращения к элементам вёрстки xml из кода, при компиляции генерировался специальный файл-"контракт" с идентификаторами элементов, а также множество методов парсинга и доступа к xml-части проекта.

И если возможность задания одного и того же файла строк несколько раз, к примеру, для разных языков - была воспринята как должное, то с вот вёрсткой возникали вопросы. Как определить, какой именно тип элемента получаем из вёрстки? Что делать, если элемента с данным id нет? А если таких элементов несколько, да ещё и в одном файле? Как быть, если элемент есть, к примеру, только в вёрстке для планшетов?

Google для этого сделал возможность задать каждому элементу вёрстки тоже иметь свой идентификатор (android:id), который тоже после компиляции был доступен из проекта, а также метод findViewById - который:

- мог возвращать null, если элемента с таким идентификатором в иерархии не найдено

- требовал ручного приведения типа возвращаемого элемента к желаемому

- внутри себя представлял ни что иное, как сложный рекурсивный поиск по иерархии

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

... обычный файл вёрстки экрана в Android
... обычный файл вёрстки экрана в Android
... и получение, и работа с этими элементами вёрстки в коде
... и получение, и работа с этими элементами вёрстки в коде

Это было и неудобно, и громоздко и выглядело ужасно. Перед работой с каждым элементом вёрстки - необходимо было вызвать для него findViewById. При этом вызове, каждый раз был риск получения NullPointerException из-за отсутствия элемента в вёрстке, или некорректного его типа.

Решением стала библиотека ButterKnife, где этот утомительный процесс был переложен на кодогенерацию:

Инициализация элементов происходит внутри метода ButterKnife.bind(this). Внутренний код метода генерируется во время компиляции, на основе аннотаций @BindView
Инициализация элементов происходит внутри метода ButterKnife.bind(this). Внутренний код метода генерируется во время компиляции, на основе аннотаций @BindView

Но из-за генерации кода, увеличивалось время компиляции проекта. К тому же, все view биндились в один момент - что плохо сказывалось на скорости работы приложения.

Так же, примерно в это время, была выпущена среда Android Studio - которая тоже пыталась помогать, как могла: автоматически компилировала файл-"контракт", делала подсказки, если элемент вёрстки отсутствовал для какой-то конфигурации девайса.

Несколько лет спустя, до проблемы добрались и разработчики kotlin из JetBrains. Появился Kotlin Android Extensions - плагин для сборщика gradle, позволяющий мгновенно получать доступ к элементу из вёрстки. Там и время компиляции меньше увеличивается, и привязка проходит при первом обращении, и кэширование есть.

Выделение жёлтым - та самая магия. Именно этот файл даёт нам переменные, названные как в нашей вёрстке, и даже меняется прямо во время редактирования вёрстки! При обращениях к элементу, под капотом он не только читается из файла вёрстки - но ещё и кэшируется для последующих обращений!
Выделение жёлтым - та самая магия. Именно этот файл даёт нам переменные, названные как в нашей вёрстке, и даже меняется прямо во время редактирования вёрстки! При обращениях к элементу, под капотом он не только читается из файла вёрстки - но ещё и кэшируется для последующих обращений!

Только вот с этой "магией" появились другие проблемы:

  • Можно импортировать не только полный файл, но и отдельный элемент вёрстки. Это позволяет по ошибке импортировать элемент вёрстки с таким-же названием из другого файла, и получить падение приложения в runtime (т.к. элемент УЖЕ будет
  • Абсолютная несовместимость со старым кодом на java.

Но тут в игру включился и сам Google. Был разработан ViewBinding - который очень напоминал подход ButterKnife - но теперь всё это было и в Java и в Kotlin, с кэшированием из коробки, никак не затрагивало компиляцию. А в коде импортировался файл, сгенерированный по макету вёрстки (что исключало обращение к элементу "не из нашего" файла).

Чем-то похоже на ButterKnife: только теперь генерируется файл ActivitySampleBinding. И уже прямо во время редактирования нашей вёрстки, не затрагивая компиляцию.
Чем-то похоже на ButterKnife: только теперь генерируется файл ActivitySampleBinding. И уже прямо во время редактирования нашей вёрстки, не затрагивая компиляцию.

Финальной строчкой стало... внедрение Jetpack Compose. Суть этой штуки в том, что вся вёрстка теперь просто создаётся из кода!

Всё! Проблема findViewById решена! Он больше просто не нужен.

Этот пример дал мне пищу для размышлений: а как же впринципе, определять "временные" технологии в разработке?
Когда обязательно стоит мигрировать проект - а когда можно расслабиться, т.к. "замена" технологии придёт через пару лет?

1) Самое главное: всегда ищите корень проблемы!
У данной проблемы корень был именно в том, что для разработчиков вёрстка и код были в разных форматах, и требовались дополнительные усилия, чтобы их "подружить". Всё!

Проблемы NullPointerException, борьба с громоздостью кода и т.д. - это всё просто следствия, которые долго исправлялись костылями.

2) Смотрите на конференциях, и в коммьюнити: какие реальные проблемы решает данная технология?
Не то, что написано в красивых доках - а для чего его тянут люди в проде, на реальных проектах? Часто бывает, что реальные возможности и нужны лишь на 10-20%. Даже в данном примере, ButterKnife добавлял множество фишек: биндинг слушателей событий, биндинг других xml-ресурсов. Но когда нашлось лучшее решение проблемы
findViewById - про это всё безжалостно забыли!

Более глобальный пример - внедрение реактивщины в android в своё время было популярно из-за неудобной работы с потоками. Реактивные фреймворки вообще являют собой отдельную пападигму и стиль программирования, давая широкие возможности почти во всех аспектах разработки.

Но как только появились корутины - многие "выдохнули", и "глубина" технологии стала не просто минусом, а одним из стимулов отказа!

Когда у технологии есть некая "главная" решаемся проблема, из-за которой большинство начало её использовать - как правило, она устаревает именно с приходом лучшего решения этой "главной" проблемы.

3) Смотрите на ... хайп проблемы!
Бывает, что какие-то технические долги весят в продуктах годами, им придумали костыли, и... просто массово используют, не придумывая нового. Часто это какая-то отдельная область, которая особо никому не нужна. В то же время, насколько критична проблема
findViewById? Есть ли статистика упавших продов, или количества ошибок при дебаге, даже в первом случае? Нет! Это просто видная, поверхностная проблема, которую благодаря простоте со временем увидели разработчики всех квалификаций - а известные в сообществе люди начали изобретать свои решения!