Цель - получение ID, уникального только в рамках конкретного устройства (то есть без гарантий уникальности между всеми устройствами, как у UUID).
Есть специфичный кейс (не из кода ВБ), например, если не доверяем бэку или есть гарантия повторов ID в списках (а они приведут к крашу, если оставить, а убирать повторы мы не хотим, так как можем случайно убрать не то), то нам нужно генерировать свой ID локально для множества элементов. Если мы не хотим добавлять логику с кэшем в SQLite (а там мы могли бы использовать автоинкремент), то получим влияние на метрику "время до контента" при генерации ID для всех элементов в списке даже на другом потоке + время до появления контента при скролле пострадает.
Также, даже если вызовов не так много, но если логика используется часто и во многих местах, то эффект накопительный вместе с другими микрооптимизациями в таких же общих компонентах. Я бы сравнил это с тем, как разработчики Jetpack Compose во время разработки новых версий делают множество микрооптимизаций (аллокаций, структур данных, алгоритмов), которые в сумме могут давать хороший прирост в каждой новой версии.
Если идти во все тяжкие и пытаться дожать оптимальную реализацию дальше, то:
Не очень понятна цель derivedState по всему коду, так как нету операций сокращения итогового множества значений. Это приводит к лишней нагрузке
Не стоит использовать State.value, как ключ к LaunchedEffect. Для треккинга можно snapshotFlow внутри LaunchedEffect слушать. Инвалидировать эффект по ключу на сам стейт (а не на . value). Сейчас может лишний раз рекомпозироваться элемент, даже если это ему не нужно в идеале. В конкретно этом случае не критично
В кастомных лейаутах стоит сперва проверять на ограниченность maxHeight/width через метод hasBounded*, если не хотите в будущем ловить непонятные краши в трудновоспроизводимых кейсах, когда туда придёт бесконечность
DrawWithCache не нужен по сути в том коде. Его для других кейсов используют. В целом можно заменить пустой Box с drawWithCache на Canvas
Многое из этого приходит только с опытом написания api для других разработчиков и чтения примеров реализации в либах. Довольно круто, что собрали всё это в одной статье. Никогда не нравилось, когда используют java-like factory-методы там, где можно обойтись красивым синтаксическим сахором, сделав апи прекраснее
Если в key не передавать ничего, то он будет использовать порядковый номер как ключ и спокойно отображать одинаковые элементы. Есть требование: ключ должен быть стабильный и уникальный. Если ничего не передать key - то порядковый номер вполне выполняет это требование. Если нам нужно обновлять список, то нужно предоставлять свой ключ для более правильной работы внутреннего механизма (а ля diffutil). И вот вы предоставили два одинаковых ключа по разным индексам. И тут начинается дискуссионный вопрос, что делать? В теории можно было бы под капотом в Compose генерировать свой псевдоключ для таких кейсов. Но откуда разрабы Compose могут быть уверены, что дальнейшее поведение на основе таких псевдоключей будет устраивать вас? Вдруг если новый псевдоключ будет конфликтовать с другим вашим ключом? И так куча разветвлений проблемных вопросов начинается. Второй вариант - как раз крашить при таком. Обоснование простое: вы нарушили контракт, а разрабы не хотят за вас думать, как вы хотели, чтобы это работало. Поэтому для даже одинаковых айтемов нужен разные ключи.
Когда в бд вы добавляете строчку с таким же id, то вы же не ругаетесь, что он крашит, хотя вам и нужно два одинаковых айтема в таблице. Если вам нужно два одинаковых id, то вы делаете ещё один столбец, который будет реальным id для таблицы и всегда уникальным, а тот id, который был у айтема изначально делаете обычным столбцом с информацией. В случае со списком ситуация аналогична.
Да, эта проблема может быть не очевидна изначально. И да, этот подход может кому-то не нравится. Но он более чистый.
В случае rv вы сами отвечаете за diff util, поэтому этот вопрос перекладывается на вас. Пока что можно только плохо относится со стороны того, что разрабы не предоставили разные стратегии. Вы можете завести им issue (либо найти существующие) и описать, что вам не нравится стратегия по умолчанию (краш) и вы хотели бы, чтобы была ещё стратегия, которая бы сама решала конфликт одинаковых ключей по разным индексам.
У нас была неожиданная для нас логика обновления Subcompose. Мы завели issue, уже в альфа 1.7 она пофикшена. Поэтому не вижу проблемы и вам предложить ваш вариант и описать как решить все его корнер кейсы.
Поэтому я и написал, что вопрос стратегии по умолчанию дискуссионный.
Мы генерируем свой id (ключ) для такого внутри класса для пагинации, так как тоже могут прийти несколько элементов с одинаковым id. Не вижу особой проблемы. В противном случае разработчикам Compose пришлось бы делать неявное поведение. И тогда это сводится к дискуссионному вопросу: крашить, но не скрывать проблему или скрывать (делая любое другое поведение), но не крашить.
Другой контракт списка относительно rv - это не плохо и не хорошо. Плохо может быть только с той стороны, что про это многие не знают, но со временем узнают) А так всё явно прописано в доке к параметру key - "дубликаты не допустимы".
Про нормально в плане использования я говорю со стороны знания, как их правильно использовать. Мне чисто приятнее использовать и кратче писать, чем в xml с rv.
Сейчас довольно легко судить с той стороны, когда rv изучен всеми от начала до конца и все знают его проблемы и как на нём не стоит писать, а как стоит. Но вы всегда можете помочь с этим сообществу и написать статью о таких особенностях ленивых списках в Compose относительно rv и как их решать, чтобы поднять общий уровень осведомлённости)
Я больше со стороны производительности и в целом использования говорил. Так как я платформенный разработчик, то могу не знать кейсы анимаций, в которых списки Compose проигрывают rv. Поэтому по моему опыту использования они уже круче и удобнее, чем rv. Но я с rv работал последний раз года два назад, будучи ещё джуном-мидлом)
99.9% - скорее в самом коде проекте. Авторизация - это внешний озоновский сдк, за которой отвечает уже не наша команда. На View в нашем коде осталось 2-3 экрана, которые давно не трогали.
Инсеты - зависят от фичи. Если это не авторизация/мессенджер, то тогда скорее всего наш код. И в нём полную поддержку инсетов планируем завести после отказа от фрагментов. Пока что точечно на костылях это делаем.
В рекомпозицию уже давно не упираемся. На них смотрели в основном в начальной стадии оптимизации, когда было много лишних рекомпозиций. Про неё была 1-ая часть статьи как раз. Сейчас уже смотрим больше в сторону разгрузки самих (ре)композиций по длительности и нагрузке, а не по числу.
По поводу замены rv - у нас в целом всё ок со списками сейчас. По лагу на время скролла на 90 перцентиле - одни нули (максимум 0.5%). Года 1.5 назад до оптимизаций и на Compose 1.1-1.2 этот показатель был 15-20%, что очень плохо. Поэтому на Compose 1.5 ленивые списки уже вполне норм. Не говоря про Compose 1.6.
Обычно это не проблема, так Image/Icon - простые элементы (в плане структуры) и когда до них доходит рекомпозиция, то в большинстве случаев она реально нужна. Избегать рекомпозицию можно за счёт того, чтобы просто не пускать её близко к ним. Например, в коде ниже мы просто следим за пропускаемостью MyItem и этого достаточно, чтобы Image лишний раз не затрагивалась.
В каких-то редких кейсам возможно полезно сделать функцию-обёртку для Image, но у нас такие кейсы не встречались.
Другое дело, если мы сами используем Painter в сложных элементах:
@Composable
fun MyComplexItem(image: Painter) {
// ...
}
В таком случаем цена рекомпозиции из-за нестабильной Painter будет высокая и часто будет происходить, когда не нужно. Это можно исправить либо сделав обёртку для Painter и пометив аннотацией, либо указав Painter стабильным с Compose Compiler 1.5.4+, либо передавать другие данные, чтобы создавать Painter внутри. Об этом написано также в этой статье.
val counter = remember { mutableStateOf(3) }
val onClick = { counter.value }
onClick()
Превратится примерно в такой:
val counter = remember { mutableStateOf(3) }
val onClick = remember(counter) { Lambda(counter) }
onClick()
В конце главы про лямбды есть ссылка на видео, где с 25 минуты объясняется, во что превращается лямда в compose. Также лямбды ещё меняются после работы R8 (подобные лямбды объединяются в один класс), но это не так важно в этом контексте, так как на выходе всё равно будет класс (или объект, если не было захвата внешних переменных)
Не очень хорошая практика применять сабкомпозицию для такой вещи. Тут есть примеры как работать с текстом и что оптимальнее https://link.medium.com/JqYqK1uHTzb
У себя реализовывали через onTextLayout и mutableState: делаем кастомный лейаут, в нём меряем текст, сразу после этого у нас mutableState не равен null и используем его для размещения. Всё происходит за один фрейм без лишней рекомпозиции, так как состояние читается при композиции, а при лейаутинге. Подробнее подход можно найти в комменте к той статье.
Цель - получение ID, уникального только в рамках конкретного устройства (то есть без гарантий уникальности между всеми устройствами, как у UUID).
Есть специфичный кейс (не из кода ВБ), например, если не доверяем бэку или есть гарантия повторов ID в списках (а они приведут к крашу, если оставить, а убирать повторы мы не хотим, так как можем случайно убрать не то), то нам нужно генерировать свой ID локально для множества элементов. Если мы не хотим добавлять логику с кэшем в SQLite (а там мы могли бы использовать автоинкремент), то получим влияние на метрику "время до контента" при генерации ID для всех элементов в списке даже на другом потоке + время до появления контента при скролле пострадает.
Также, даже если вызовов не так много, но если логика используется часто и во многих местах, то эффект накопительный вместе с другими микрооптимизациями в таких же общих компонентах. Я бы сравнил это с тем, как разработчики Jetpack Compose во время разработки новых версий делают множество микрооптимизаций (аллокаций, структур данных, алгоритмов), которые в сумме могут давать хороший прирост в каждой новой версии.
Если идти во все тяжкие и пытаться дожать оптимальную реализацию дальше, то:
Не очень понятна цель derivedState по всему коду, так как нету операций сокращения итогового множества значений. Это приводит к лишней нагрузке
Не стоит использовать State.value, как ключ к LaunchedEffect. Для треккинга можно snapshotFlow внутри LaunchedEffect слушать. Инвалидировать эффект по ключу на сам стейт (а не на . value). Сейчас может лишний раз рекомпозироваться элемент, даже если это ему не нужно в идеале. В конкретно этом случае не критично
В кастомных лейаутах стоит сперва проверять на ограниченность maxHeight/width через метод hasBounded*, если не хотите в будущем ловить непонятные краши в трудновоспроизводимых кейсах, когда туда придёт бесконечность
DrawWithCache не нужен по сути в том коде. Его для других кейсов используют. В целом можно заменить пустой Box с drawWithCache на Canvas
Многое из этого приходит только с опытом написания api для других разработчиков и чтения примеров реализации в либах. Довольно круто, что собрали всё это в одной статье. Никогда не нравилось, когда используют java-like factory-методы там, где можно обойтись красивым синтаксическим сахором, сделав апи прекраснее
Если в key не передавать ничего, то он будет использовать порядковый номер как ключ и спокойно отображать одинаковые элементы. Есть требование: ключ должен быть стабильный и уникальный. Если ничего не передать key - то порядковый номер вполне выполняет это требование. Если нам нужно обновлять список, то нужно предоставлять свой ключ для более правильной работы внутреннего механизма (а ля diffutil). И вот вы предоставили два одинаковых ключа по разным индексам. И тут начинается дискуссионный вопрос, что делать?
В теории можно было бы под капотом в Compose генерировать свой псевдоключ для таких кейсов. Но откуда разрабы Compose могут быть уверены, что дальнейшее поведение на основе таких псевдоключей будет устраивать вас? Вдруг если новый псевдоключ будет конфликтовать с другим вашим ключом? И так куча разветвлений проблемных вопросов начинается.
Второй вариант - как раз крашить при таком. Обоснование простое: вы нарушили контракт, а разрабы не хотят за вас думать, как вы хотели, чтобы это работало. Поэтому для даже одинаковых айтемов нужен разные ключи.
Когда в бд вы добавляете строчку с таким же id, то вы же не ругаетесь, что он крашит, хотя вам и нужно два одинаковых айтема в таблице. Если вам нужно два одинаковых id, то вы делаете ещё один столбец, который будет реальным id для таблицы и всегда уникальным, а тот id, который был у айтема изначально делаете обычным столбцом с информацией. В случае со списком ситуация аналогична.
Да, эта проблема может быть не очевидна изначально. И да, этот подход может кому-то не нравится. Но он более чистый.
В случае rv вы сами отвечаете за diff util, поэтому этот вопрос перекладывается на вас.
Пока что можно только плохо относится со стороны того, что разрабы не предоставили разные стратегии. Вы можете завести им issue (либо найти существующие) и описать, что вам не нравится стратегия по умолчанию (краш) и вы хотели бы, чтобы была ещё стратегия, которая бы сама решала конфликт одинаковых ключей по разным индексам.
У нас была неожиданная для нас логика обновления Subcompose. Мы завели issue, уже в альфа 1.7 она пофикшена. Поэтому не вижу проблемы и вам предложить ваш вариант и описать как решить все его корнер кейсы.
Поэтому я и написал, что вопрос стратегии по умолчанию дискуссионный.
Мы генерируем свой id (ключ) для такого внутри класса для пагинации, так как тоже могут прийти несколько элементов с одинаковым id. Не вижу особой проблемы. В противном случае разработчикам Compose пришлось бы делать неявное поведение. И тогда это сводится к дискуссионному вопросу: крашить, но не скрывать проблему или скрывать (делая любое другое поведение), но не крашить.
Другой контракт списка относительно rv - это не плохо и не хорошо. Плохо может быть только с той стороны, что про это многие не знают, но со временем узнают)
А так всё явно прописано в доке к параметру key - "дубликаты не допустимы".
Про нормально в плане использования я говорю со стороны знания, как их правильно использовать. Мне чисто приятнее использовать и кратче писать, чем в xml с rv.
Сейчас довольно легко судить с той стороны, когда rv изучен всеми от начала до конца и все знают его проблемы и как на нём не стоит писать, а как стоит. Но вы всегда можете помочь с этим сообществу и написать статью о таких особенностях ленивых списках в Compose относительно rv и как их решать, чтобы поднять общий уровень осведомлённости)
Я больше со стороны производительности и в целом использования говорил. Так как я платформенный разработчик, то могу не знать кейсы анимаций, в которых списки Compose проигрывают rv. Поэтому по моему опыту использования они уже круче и удобнее, чем rv. Но я с rv работал последний раз года два назад, будучи ещё джуном-мидлом)
99.9% - скорее в самом коде проекте. Авторизация - это внешний озоновский сдк, за которой отвечает уже не наша команда. На View в нашем коде осталось 2-3 экрана, которые давно не трогали.
Инсеты - зависят от фичи. Если это не авторизация/мессенджер, то тогда скорее всего наш код. И в нём полную поддержку инсетов планируем завести после отказа от фрагментов. Пока что точечно на костылях это делаем.
В рекомпозицию уже давно не упираемся. На них смотрели в основном в начальной стадии оптимизации, когда было много лишних рекомпозиций. Про неё была 1-ая часть статьи как раз. Сейчас уже смотрим больше в сторону разгрузки самих (ре)композиций по длительности и нагрузке, а не по числу.
По поводу замены rv - у нас в целом всё ок со списками сейчас. По лагу на время скролла на 90 перцентиле - одни нули (максимум 0.5%). Года 1.5 назад до оптимизаций и на Compose 1.1-1.2 этот показатель был 15-20%, что очень плохо. Поэтому на Compose 1.5 ленивые списки уже вполне норм. Не говоря про Compose 1.6.
Ozon Seller - для продавцов. Там 99.9% компоуза. Сейчас в процессе отказа от фрагментов.
Обычно это не проблема, так Image/Icon - простые элементы (в плане структуры) и когда до них доходит рекомпозиция, то в большинстве случаев она реально нужна.
Избегать рекомпозицию можно за счёт того, чтобы просто не пускать её близко к ним.
Например, в коде ниже мы просто следим за пропускаемостью MyItem и этого достаточно, чтобы Image лишний раз не затрагивалась.
В каких-то редких кейсам возможно полезно сделать функцию-обёртку для Image, но у нас такие кейсы не встречались.
Другое дело, если мы сами используем Painter в сложных элементах:
В таком случаем цена рекомпозиции из-за нестабильной Painter будет высокая и часто будет происходить, когда не нужно. Это можно исправить либо сделав обёртку для Painter и пометив аннотацией, либо указав Painter стабильным с Compose Compiler 1.5.4+, либо передавать другие данные, чтобы создавать Painter внутри. Об этом написано также в этой статье.
Привет, первая - draw io, вторая и третья взята из ресурсов Jetpack Compose (вторая просто через фотошоп переведена)
Просто не хотел удлинять и усложнять статью ещё и детальным описанием преобразованием лямбды в котлине
Обе вещи. Такой код внутри composable-функции:
Превратится примерно в такой:
В конце главы про лямбды есть ссылка на видео, где с 25 минуты объясняется, во что превращается лямда в compose. Также лямбды ещё меняются после работы R8 (подобные лямбды объединяются в один класс), но это не так важно в этом контексте, так как на выходе всё равно будет класс (или объект, если не было захвата внешних переменных)
Привет, здесь работает принцип отложенного чтения через лямбду.
Для лямбды
{ counter }
вMyComposable2({ counter })
сгенерируется подобный код (условно, не точно такой):И уже состояние прочитается в MyComposable2, так как именно там лямбда вызовется.
В тоже время если передавать состояние так:
то это превратится в
так как делегат по сути скрывает .value от нас, и чтение произойдёт уже в MyComposable1
*так как состояние читается не при композиции, а при лейаутинге
Не очень хорошая практика применять сабкомпозицию для такой вещи. Тут есть примеры как работать с текстом и что оптимальнее https://link.medium.com/JqYqK1uHTzb
У себя реализовывали через onTextLayout и mutableState: делаем кастомный лейаут, в нём меряем текст, сразу после этого у нас mutableState не равен null и используем его для размещения. Всё происходит за один фрейм без лишней рекомпозиции, так как состояние читается при композиции, а при лейаутинге. Подробнее подход можно найти в комменте к той статье.