Цель этого поста — понять, как работает рекомпозиция, на коротком примере из реальной жизни, точно так же, как это делается, когда вы начинаете работать над новой функцией. Ломать вещи куда веселее, чем читать документы.

Давайте потеряем несколько клеток мозга вместе, хорошо?

Установка

Вот типичный «корневой» компонуемый.

Задача viewmodel.updateState() состоит в том, чтобы сгенерировать новую случайную state строку. Это, по сути, вызовет рекомпозицию корневого компонуемого.

Но как насчет его детей?

Первый проход

Объедините все в один компонуемый объект, чтобы получить что-то на экране.

Это немногонеаккуратно, но это работает.

Инспектор макетов здесь очень полезен, чтобы понять, что происходит. Он показывает, когда составные элементы в иерархии макетов были перекомпонованы или пропущены.

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

При нажатии на первый Button, state будет обновлен и вызовет перекомпоновку на MainContent.

Теперь давайте остановимся и подумаем. Какие составные объекты предполагается перекомпоновать?

Первый Text использует state. Должен обязательно перекомпоноваться.

Первый Button также должен перекомпоноваться. Дважды на самом деле. Один раз при печати и один раз при выпуске.

Реальность?

Это странно. Все компонуемые, которые не имеют ничего общего с state, всегда должны пропускать рекомпозицию.

Почему второй, совершенно не связанный с этим, Button пересочиняет?

Лямбда шмамбда

В мире композиций одни функции более равны, чем другие. 🫠

Ссылки на методы работают лучше, чем лямбда-выражения. Давайте попробуем это.

Эй, вторая кнопка теперь пропускает перекомпоновку.

Улучшить 🔍

Этого достаточно для начала. Однако есть одна проблема.

Поскольку все объединено в один большой компонуемый объект, каждый прямой дочерний элемент оценивается на предмет рекомпозиции.

Это связано с тем, что Column является встроенным компонуемым (то же самое для других типов контейнеров, например Box). Он не имеет области рекомпозиции.

Это также можно наблюдать в инспекторе компоновки. Column нет собственных счетчиков перекомпоновки/пропуска.

Пора перестать быть небрежным и создавать больше составных частей.

Второй проход

Весь этот макет можно разделить на 3 разные части.

Дети, которые представляют каждую часть:

Все вновь созданные пользовательские составные объекты теперь имеют собственную область рекомпозиции. Если входы не изменились, перекомпоновку следует пропустить.

Инспектор компоновки просит не согласиться.

Хорошо, входы FirstComposable и SecondComposable не изменились. Почему они были перекомпонованы?!

Поскольку особого смысла в этом нет, давайте копнем глубже с метриками компилятора.

Метрики компилятора

Если вы еще этого не сделали, настоятельно рекомендую прочитать прекрасную статью Composable metrics Криса Бейнса.

TLDR: компилятор компоновщика может экспортировать метрики того, насколько «эффективны» компонуемые объекты.

С точки зрения непрофессионала — в зависимости от компонуемых параметров компилятор будет знать заранее, чтобы избежать рекомпозиции, если входные данные не изменились.

Что было бы идеально? Компилятору очень нравятся составные объекты, объявленные как restartable skippable в файле ...-composables.txt.

Давай попробуем

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

Все компонуемые restartable skippable.

Прав ли инспектор компоновки? Отчет компилятора неправильный?

Мы просто видим странные вещи из-за использования инспектора компоновки в варианте debug, который не использует полностью оптимизированный код компоновки?

Таким образом, я ссылаюсь на закон Каннингема, публикуя здесь неправильный ответ в попытке получить исправление в комментариях.

Мы должны идти глубже

Хорошо, есть еще одна вещь, которую нужно попробовать.

В этой отличной статье Джастина Брейтфеллера предлагается использовать запоминаемые лямбда-выражения.

(может быть, мне следует быть в пабе вместо того, чтобы произносить фразу «вспомнил лямбды» в пятницу вечером, но сейчас уже слишком поздно)

Проверив инспектор компоновки в последний раз, ничего не должно быть перекомпоновано, кроме TextThatDisplaysState и Button, по которому щелкнули.

В идеале, некоторые компонуемые не должны даже оцениваться для перекомпоновки, так как их родительский компонуемый объект вообще пропускается.

Эй, это сработало!

Приостановить функции как составные параметры

Компилятор compose даже любитприостанавливать функции?

Помните: с SecondComposable никогда не взаимодействуют, и это не имеет ничего общего с state. Нажимается только Button внутри FirstComposable.

Кажется, что функции приостановки не являются идеальными параметрами для компонуемых. SecondComposable и его дочерний элемент Button перекомпоновываются без всякой причины.

Я думаю, это имеет смысл? Функции подвески довольно сложны под капотом.

Метрики компилятора также подтверждают это, помечая компонуемый только как restartable, а не skippable.

Даже если функция приостановки используется только внутри LaunchedEffect или просто передается вниз по течению, перекомпоновка не будет пропущена.

Дети, которые не имеют ничего общего с функцией приостановки, будут соответствующим образом пропущены, так что на самом деле все не так уж и плохо.

Приложение

Если вы зашли так далеко, вы можете подумать: а стоит ли все это того?

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

С одной стороны, стремление к «идеальным» составным элементам — бесполезное занятие, особенно в большой кодовой базе с частыми выпусками функций. Сам код тоже становится довольно сложным для чтения, а преимущества спорны.

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

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

Итак, что делать?

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

Тем не менее, метрики компилятора (имхо) превосходят инспектора компоновки, поскольку они не требуют ручного тестирования на устройстве.

Основная проблема с метриками компилятора заключается в запуске команды gradle в сборке релиза с --rerun-tasks, чтобы убедиться, что компилятор Compose работает даже при кэшировании.

Это действительно может занять некоторое время, если вы работаете на не очень быстрой машине, которая побеждает цикл быстрой итерации/обратной связи.

В любом случае

Надеюсь, вы нашли это несколько полезным.

@ маркасдубликат

Позже.