Организация компонентов в Jetpack Compose: Работа над ошибками
В посте про организацию компонентов в Jetpack Compose показывал вариант с типизированными слотами, которые представляют собой State и позволяют реализовать "умную" рекомпозицию функции в отдельных слотах за счет donut-hole skipping.
Рекомендую ознакомиться с частью 4 из того поста под название "Слот как State" перед тем, как читать дальше.
В том решении было лишь примерно то, как может выглядеть компонент. В реальности Avatar и другие компоненты намного сложнее, что приводит к тому, что внутрянка слотов разрастается стремительно.
Очевидно надо было думать, как это раскидать в отдельные файлы, сохранив при этом преимущества подхода с типизированными слотами. К счастью, структура c использованием sealed inteface/class, которая лежала внутри одного модуля, давала возможность отрефакторить это дело позже, так как у клиентов не было возможности раширить слоты.

И вот последней каплей для начала рефакторинга стал Code Completition в Android Studio. При попытке импортировать функцию remember, появлялось такое громадное окошко, где нужный remember был в самом конце.

Начал думать, как бы теперь реализовать слоты. Эврика появилась, когда попался на статью про конструкторы-самозванцы и пост в канале Ra`Reilly. Думаю, многие были в курсе про эти фичи Kotlin'а и, скорее всего, даже использовали. Но иногда так бывает, что инсайты в голове задерживаются пока не встретишь структурированную информацию, которая сыграет роль катализатора для новых идей. Спасибо авторам за это!
Итак, в том решении меня не устраивали:
- использование
rememberфункции для создания слота - то, как файл со слотами разрастается
В этот раз примером будет баннер, который состоит из трех частей: левой, средней и правой. Каждая часть может отображать только определенный набор слотов.

Дизайнеры так сделали этот компонент, что им достаточно пару кликов в фигме для настройки. Нам нужно так же. Чтобы разработчик не думал о реализации, а просто взял, вставил в верстку и пошел делать дела поважнее и посложнее.
1. Сделал сначала согласно варианту, который устраивал меня в предыдущем посте:

Тут намеренно свернул все классы, так как каждый представляет из себя примерно следующее:

2. Для того, чтобы уменьшить количество remember, заменил его на operator invoke:

Благодаря этому использование слота стало даже приятнее. И зачем я придумал называть функцию remember...

После того, как прошелся по всем слотам множества компонентов, в проекте остался один единственный remember, который настоящий. Бонусом AS даже не показывает диалог с выбором функции для импортирования. Стало лучше, но надо решить проблему с размером файла.
2. С решением этой проблемы поможет то, что мы можем писать extension функции для companion object. Да, нашел этому свое применение :)
Сначала вытащил всю реализацию слотов из этого файла и оставил лишь описание иерархии. Далее к каждому слоту, реализацию которого нужно сделать, добавил пустой companion object.

Получился своего рода контракт компонента, описывающий что и куда можно положить. Кажется, что такое можно даже попробовать генерировать.
Ну и реализацию слота унес в отдельный файл, а рядом положил extension к companion object.

Из минусов то, что теперь этот invoke нужно импортировать в месте, где используется слот.
Итог
Подход со слотами по-прежнему является очень удобным для некоторых компонентов с учетом количества требований к ним в дизайн системе. Сейчас его получилось причесать за счет использование относительно спорных конструкции языка. Уж очень они тут пригодились :)
Upd.
Как заметили в комментариях к посту, возможно, что я перестарался с выносом кода из слотов. Кроме того, что появился дополнительный импорт функции invoke, появилась дополнительная проблема с пониманием того, как использовать слоты, так как extension функции спрятаны, поэтому нужно постоянно держать в голове, что где-то extension функция invoke.
В итоге, слоты описывали ЧТО можно положить в компонент, но никак не подсказывали КАК конкретный слот использовать.
Для решения решения проблемы стоит вернуть вызов invoke в companion object и в нем сделать проксирование в функцию remember, которая будет internal:

Сама реализация слота будет выглядеть следующим образом:

Итог 2
Вернули некоторые детали в файл с описанием слотов, но зато это помогло избавиться от дополнительного импорта, а так же позволит разработчикам открыв этот файл понять как именно можно использовать слоты для конкретного компонента.
Опубликовано в Полуночные Зарисовки