惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Schneier on Security
Schneier on Security
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 三生石上(FineUI控件)
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
The GitHub Blog
The GitHub Blog
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
Blog — PlanetScale
Blog — PlanetScale
T
The Blog of Author Tim Ferriss
罗磊的独立博客
H
Help Net Security
博客园 - 聂微东
Apple Machine Learning Research
Apple Machine Learning Research
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
云风的 BLOG
云风的 BLOG
博客园 - 【当耐特】
G
GRAHAM CLULEY
S
Schneier on Security
A
About on SuperTechFans
MyScale Blog
MyScale Blog
Stack Overflow Blog
Stack Overflow Blog
T
The Exploit Database - CXSecurity.com
Y
Y Combinator Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Register - Security
The Register - Security
P
Proofpoint News Feed
Jina AI
Jina AI
Latest news
Latest news
T
Threat Research - Cisco Blogs
V
Visual Studio Blog
P
Privacy International News Feed
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
T
Tor Project blog
SecWiki News
SecWiki News
Google Online Security Blog
Google Online Security Blog
O
OpenAI News
T
Tenable Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Почему я написал Paginator вместо Paging 3
Jamal_Aliev · 2026-05-01 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение7 мин

Охват и читатели689

Мнение

Это статья от автора библиотеки, поэтому нейтральным разбор не будет. Но это и не рассказ про
конкретный проект — а разбор задач, на которых, на мой взгляд, Paging 3 начинает буксовать, и
того, как Paginator устроен, чтобы эти задачи
закрывать. KMP-библиотека пагинации для Android, iOS, JVM и Desktop. Ниже — почему она появилась
именно как отдельная библиотека, а не как fork или обёртка над Paging 3.

Сцена, в которой Paging 3 перестаёт быть удобным

Возьмите список требований, которые рано или поздно прилетают в любую ленту, чат или поисковую
выдачу — обычно не на старте, а через несколько месяцев работы продукта:

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

В каждом из этих пунктов по отдельности Paging 3 не сдаётся — есть RemoteMediator, есть
cachedIn, есть способы выкрутиться. Но если они приходят пакетом — а в современном продукте
они приходят пакетом, — выкручивание занимает больше времени, чем сама фича. И в какой-то
момент команда замечает, что больше думает о том, как заставить PagingSource адресовать
страницу, а не ключ, чем о том, как должен вести себя экран.

С этого момента Paging 3 уже не инструмент, а ограничение, к которому подгоняется задача. Это
плохой знак — и именно ту точку, в которой он наступает, я и пытался отодвинуть, проектируя
Paginator.

Где Paging 3 начинает проседать

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

PagingSource адресует ключами, а не страницами. Это разумно, когда лента листается
курсорами вперёд-назад и неважно, какой по счёту элемент. Но как только в задаче появляется
«открой страницу с сообщением msg_817» — оказывается, что прыгнуть можно только в страницу,
ключ которой уже знаком. Канонический ответ — Room как источник правды и RemoteMediator,
который синхронизирует БД с сетью. Для проектов, где БД и так есть, это нормально. Если её
нет, появление БД приходится обосновывать только ради того, чтобы пагинация работала. Это
перевернутая зависимость: продуктовый сценарий начинает диктовать архитектуру слоя данных.

PagingData иммутабельна и заточена под UI. Когда задача требует поменять один элемент в
ленте — поставить лайк, обновить статус «прочитано», локально удалить — прямого пути нет.
PagingData — это поток событий для адаптера, а не структура, которой можно сказать «замени
элемент с id = 42». Стандартное решение опять упирается в Room: меняем строку, инвалидация
дёрнет адаптер. Если бэкенд эфемерный и БД нет — её нужно завести. Если хочется оптимистично
применить мутацию до ответа сервера — нужно расставить флаги в строках БД. Каждое следующее
требование добавляет к решению ещё один слой.

Pagination живёт в UI-слое. Если придерживаться клин-архитектуры в умеренном виде —
пагинация это поведение домена, а не UI. И если три экрана делят одну и ту же ленту, удобно
иметь один объект пагинации в use-case'е, на который подписаны три ViewModel. С
Flow<PagingData<T>> это превращается в задачу — поток одноразовый, привязан к scope, и
таскать его через слои значит тащить вместе с ним UI-абстракции в домен.

iOS просто нет. В апстрим-исходниках paging-common KMP-таргеты постепенно появляются, и это
хорошая новость на будущее. Но публикуемые артефакты, инструменты вокруг (Room, RecyclerView,
Compose-адаптеры) и реальная экосистема — Android-first. Когда нужно дать iOS-команде то же
самое поведение пагинации, что и Android, ответа из коробки нет.

Каждый из этих четырёх пунктов решаем по отдельности. Все четыре одновременно — нет.

Почему не fork и не обёртка

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

Ядро Paging 3 устроено вокруг pull-модели потока событий для UI. Чтобы превратить его в
обычный объект состояния, который живёт в домене и которым можно управлять снаружи (jump,
replace, serialize), нужно перепридумать почти все ключевые типы — PagingSource,
PagingData, LoadStates. После такой переделки от исходной библиотеки остаётся, по сути,
только название.

Wrapper тем более не работает: обёртка вокруг Flow<PagingData<T>> всё равно остаётся обёрткой
вокруг иммутабельного потока. Через неё нельзя дать ни мутацию элемента, ни сериализацию кэша,
ни прыжок на произвольную страницу — потому что под обёрткой этих операций нет.

Поэтому Paginator — не альтернатива в смысле «тот же подход, но иначе», а другая модель:
страница это адресуемая ячейка в кэше, кэш это обычная структура данных, навигация это обычные
методы.

Принцип 1. Страница — адресуемая

В Paging 3 движение по ленте — это движение по ключам: «дай страницу с этим cursor'ом», «дай
следующую». В Paginator страница адресуется напрямую — номером, курсором или закладкой:

paginator.goNextPage()
paginator.goPreviousPage()
paginator.jump(BookmarkInt(page = 42))
paginator.jump(CursorBookmark(self = "msg_817"))

Это маленькое решение, но оно меняет очень многое. Открыть переписку на сообщении из push —
одна строка. Запомнить позицию пользователя и вернуться через сутки — одна строка. Поставить
именованную закладку «начало непрочитанных» и циклически прыгать между закладками с
jumpForward / jumpBack — встроенный механизм, а не самописный поверх состояния адаптера.

У адресуемости есть ещё одно следствие: страницы можно складывать в обычную сортированную
коллекцию и работать с ней как с коллекцией. Снапшот — это упорядоченный список загруженных
страниц, и его можно отдать в UI напрямую, без посредников вроде LazyPagingItems. Если
хочется — можно подписаться на готовый paginator.uiState: Flow<PaginatorUiState<T>>, в
котором уже свернуты состояния Idle / Loading / Empty / Error / Content(items, prependState, appendState) для типовых экранов.

Принцип 2. Мутабельность по запросу

В Paginator два интерфейса, и разделение между ними сделано осознанно.

Paginator — read-only навигация. Его можно отдать в UI или в любой компонент, который не
должен менять данные. Никаких set, replace, remove в публичном API — компилятор
гарантирует, что место чтения не поломает страницу.

MutablePaginator — расширение, в котором появляются операции CRUD над элементами:

mutablePaginator.replace(predicate = { it.id == 42 }, transform = { it.copy(liked = true) })
mutablePaginator.removeWhere { it.deleted }
mutablePaginator.insertAfter(target = anchor, element = newMessage)

После каждой операции страницы автоматически перебалансируются по capacity, кэш остаётся
консистентным, снапшот переиспускается. Никакой инвалидации, никакого «перезагрузим страницу».
Один лайк меняет один элемент.

Этот же механизм закрывает оптимистичные обновления: применили мутацию локально, отправили
запрос, при ошибке откатили через transaction { } — атомарный блок, в котором все изменения
откатываются целиком при любом исключении, включая отмену корутины. Это снимает целый класс
багов, связанных с расхождением UI и сервера в момент сетевой ошибки.

Принцип 3. Состояние, которое сохраняется

Process death — старая беда Android. cachedIn(viewModelScope) в Paging 3 живёт ровно столько,
сколько живёт scope, а scope умирает вместе с процессом. После возврата scroll position может
восстановиться, но данные подгружаются заново — и хорошо, если бэкенд возвращает их в том же
порядке.

В Paginator кэш — обычная структура данных, и она с самого начала проектировалась так, чтобы её
можно было сериализовать:

val saved: String = paginator.serializeToJson()
// ... process death ...
paginator.restoreFromJson(saved)

Сериализация работает через kotlinx.serialization, поэтому едет на любой KMP-таргет —
Android, iOS, Desktop. На выходе получается не «верни на ту же позицию», а «верни в то же
состояние, со всеми видимыми страницами и текущей точкой». Пользователь, открывший экран по
deeplink, после убийства процесса возвращается к тому же сообщению с теми же соседями вокруг —
и без сетевого запроса.

Это не магия — это следствие того, что состояние пагинации хранится отдельно от Flow и не
зависит от жизненного цикла UI.

Принцип 4. Библиотека, а не фреймворк

Paginator написан на чистом Kotlin без платформенных зависимостей. Это значит несколько
практически важных вещей.

Во-первых, он живёт в commonMain. Один и тот же код пагинации работает на Android, iOS,
Desktop и сервере. Не «когда-нибудь будет», а сейчас, в публикуемых артефактах. Логика
пагинации становится частью общего доменного слоя KMP-проекта — без копирования между
платформами и без двух отдельных реализаций «как у нас принято».

Во-вторых, его не обязательно держать в ViewModel. Pagination — это поведение, а не
UI-состояние, и оно нормально живёт в use-case'е или репозитории. Один Paginator в
data-слое — три ViewModel, подписанные на него. Тот же Paginator можно прокинуть в worker
для фоновой подгрузки. Юнит-тестировать его можно без runTest-обвязки и без mock'ов
AndroidX.

В-третьих, его легко вырезать. Если завтра окажется, что вам подходит другое решение —
Paginator локализован в одном месте архитектуры и не размазан по UI и БД. Это редко
обсуждаемое, но важное свойство любой библиотеки: не только удобно подключаться, но и удобно
отключаться.

Принцип 5. Cursor — равноправный, а не workaround

В Paging 3 курсорная пагинация делается как частный случай PagingSource с ключом-строкой.
Работает, но требует руками собрать модель prev / self / next для GraphQL-style
connections и аккуратно ловить EndOfPaginationReached.

В Paginator курсорная пагинация сделана равной по статусу: CursorPaginator рядом с
Paginator, одинаковая модель состояний, одинаковый uiState, одинаковый transaction { },
одинаковая сериализация. Различается только адресация — вместо номера страницы используется
тройка prev / self / next:

val messages = mutableCursorPaginator<Message> {
    load { cursor ->
        val page = api.getMessages(cursor?.self as? String)
        CursorLoadResult(
            data = page.items,
            bookmark = CursorBookmark(prev = page.prev, self = page.self, next = page.next),
        )
    }
}

Для GraphQL connections, чатов и activity-стримов это правильная модель из коробки — без
переизобретения колеса поверх LoadParams.

Что есть сейчас

На момент публикации статьи Paginator опубликован на Maven Central под версией 8.5.0.
Поддерживаемые таргеты — Android, JVM (Desktop / Server), iosX64, iosArm64, iosSimulatorArm64.
Есть отдельный артефакт paginator-compose с биндингами для Jetpack Compose / Compose
Multiplatform — он добавляет одну строку scroll-driven prefetch для LazyColumn / LazyRow /
LazyVerticalGrid и их вариантов, без ручного LaunchedEffect / snapshotFlow.

Документация лежит в docs/
репозитория, разбита по темам — от core concepts до cursor pagination и interweaving. Если
хочется быстрого ощущения —
есть демо APK.

Когда Paginator вам, скорее всего, не нужен

Если у вас простая лента, источник правды — Room, и продуктовые требования стабильны:
подгрузка вниз, pull-to-refresh, и больше ничего сверху не предвидится — Paging 3 у вас, скорее
всего, уже работает, и менять его ради смены смысла нет. Это другой класс задач, и в этом
классе Paginator не даст ощутимого выигрыша.

Ссылки