{"id":477312,"date":"2026-04-24T17:27:28","date_gmt":"2026-04-24T17:27:28","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=477312"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=477312","title":{"rendered":"\u041c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440 \u043d\u0430 Paginator. \u0411\u043e\u0435\u0432\u044b\u0435 \u0437\u0430\u0434\u0430\u0447\u0438"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412 <a href=\"https:\/\/habr.com\/ru\/articles\/1027320\/\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435<\/a> \u044f \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043b Paginator \u0441 Paging 3 \u043d\u0430 \u043a\u043e\u0448\u0430\u0447\u044c\u0435\u043c \u0443\u0440\u043e\u0432\u043d\u0435: \u00ab\u0432\u043e\u0442 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0444\u0438\u0434, \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u2014 \u0442\u0440\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u043c\u0435\u0441\u0442\u043e \u0442\u0440\u0438\u0434\u0446\u0430\u0442\u0438\u00bb. \u042d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0434\u043b\u044f \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0437\u043d\u0430\u043a\u043e\u043c\u0441\u0442\u0432\u0430, \u043d\u043e \u043d\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043d\u0430 \u0433\u043b\u0430\u0432\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441: <strong>\u0430 \u043a\u0430\u043a \u043e\u043d\u043e \u0441\u0435\u0431\u044f \u043f\u043e\u0432\u0435\u0434\u0451\u0442, \u043a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0442 \u043d\u0430\u0447\u043d\u0451\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0442\u043e, \u0440\u0430\u0434\u0438 \u0447\u0435\u0433\u043e \u043b\u044e\u0434\u0438 \u043e\u0431\u044b\u0447\u043d\u043e \u0438 \u043f\u0438\u0448\u0443\u0442 \u0441\u0432\u043e\u0439 \u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434 \u043f\u043e\u0432\u0435\u0440\u0445 Paging 3?<\/strong><\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0431\u0435\u0440\u0443 \u043c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440 \u2014 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440 \u044d\u0442\u043e \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u0433\u043e\u043d. \u0422\u0430\u043c \u0435\u0441\u0442\u044c:<\/p>\n<ul>\n<li>\n<p>\u043b\u0435\u043d\u0442\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0441 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u043e\u0439 \u0432\u0432\u0435\u0440\u0445 \u0438 \u0432\u043d\u0438\u0437,<\/p>\n<\/li>\n<li>\n<p>\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u043d\u0430 \u0441\u043a\u0440\u043e\u043b\u043b\u0435 (prefetch) \u0431\u0435\u0437 \u043a\u043d\u043e\u043f\u043e\u043a \u00ab\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0449\u0451\u00bb,<\/p>\n<\/li>\n<li>\n<p>\u043d\u043e\u0432\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0438\u0437 WebSocket \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438,<\/p>\n<\/li>\n<li>\n<p>optimistic send \u0441 \u043e\u0442\u043a\u0430\u0442\u043e\u043c \u043f\u0440\u0438 \u043e\u0448\u0438\u0431\u043a\u0435,<\/p>\n<\/li>\n<li>\n<p>\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435,<\/p>\n<\/li>\n<li>\n<p>deeplink \u043d\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438 \u043f\u0440\u044b\u0436\u043a\u0438 \u043d\u0430 \u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u044b\u0435,<\/p>\n<\/li>\n<li>\n<p>date-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 \u0438 \u043f\u043b\u0430\u0448\u043a\u0430 \u00ab\u041d\u043e\u0432\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u00bb,<\/p>\n<\/li>\n<li>\n<p>\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u043a\u0438 (\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e, \u0441 \u043e\u0442\u043a\u0430\u0442\u043e\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435),<\/p>\n<\/li>\n<li>\n<p>\u0440\u0430\u0431\u043e\u0442\u0430 \u043e\u0444\u0444\u043b\u0430\u0439\u043d \u0441 \u043f\u0435\u0440\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435\u043c process death.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u0435\u0432\u044f\u0442\u044c \u0431\u043e\u0435\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447. \u041e\u0434\u043d\u0430 ViewModel. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u043a\u043e\u0441\u0442\u044b\u043b\u0435\u0439.<\/p>\n<h3>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440 \u043f\u0440\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u0443\u044e \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044e<\/h3>\n<p>\u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0447\u043d\u0451\u043c: \u0435\u0441\u043b\u0438 \u0432\u0430\u0448 \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043d\u0435 \u043f\u043e \u043d\u043e\u043c\u0435\u0440\u0443 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b, \u0430 \u043f\u043e <code>nextCursor<\/code> \/ <code>prevCursor<\/code> (GraphQL connections, Slack API, Instagram, Reddit \u0438 \u043f\u0440\u043e\u0447\u0438\u0435 \u043b\u0435\u043d\u0442\u044b \u0441 \u00ab\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u044b\u043c \u043a\u0440\u0430\u0435\u043c\u00bb), \u2014 \u0432\u0430\u043c \u043d\u0443\u0436\u0435\u043d \u043d\u0435 <code>Paginator<\/code>, \u0430 \u0435\u0433\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u044b\u0439 \u0431\u0440\u0430\u0442 <code>CursorPaginator<\/code>.<\/p>\n<p>\u042d\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u044b \u0438 Int-\u0438\u043d\u0434\u0435\u043a\u0441\u044b \u0436\u0438\u0432\u0443\u0442 \u043f\u043e \u0440\u0430\u0437\u043d\u044b\u043c \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c: \u0443 \u043a\u0443\u0440\u0441\u043e\u0440\u0430 \u043d\u0435\u0442 \u00ab\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b 42\u00bb, \u043d\u0435\u0442 random-access \u043f\u0440\u044b\u0436\u043a\u043e\u0432 \u043d\u0430 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440, \u043d\u0435\u0442 <code>resize(capacity)<\/code>. \u0417\u0430\u0442\u043e \u0435\u0441\u0442\u044c <code>CursorBookmark(prev, self, next)<\/code> \u0438 LinkedList-\u043c\u043e\u0434\u0435\u043b\u044c, \u0433\u0434\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u043d\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u0432\u043e\u0438\u0445 \u0441\u043e\u0441\u0435\u0434\u0435\u0439.<\/p>\n<p>API \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u2014 \u0437\u0435\u0440\u043a\u0430\u043b\u044c\u043d\u043e\u0435:<\/p>\n<pre><code class=\"kotlin\">val paginator = mutableCursorPaginator&lt;Message&gt;(capacity = 50) {    load { cursor -&gt;        val page = api.getMessages(cursor?.self as? String)        CursorLoadResult(            data = page.items,            bookmark = CursorBookmark(                prev = page.prevCursor,                self = page.selfCursor,                next = page.nextCursor,            ),        )    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0435 \u0436\u0435 <code>uiState<\/code>, <code>jump<\/code>, <code>goNextPage<\/code>, <code>interweave<\/code>, <code>transaction<\/code>, L2-\u043a\u044d\u0448 \u2014 \u0432\u0441\u0451 \u043d\u0430 \u043c\u0435\u0441\u0442\u0435. \u041f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0438\u0437 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u044f\u0442\u0441\u044f \u043e\u0434\u0438\u043d-\u0432-\u043e\u0434\u0438\u043d, \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u044e\u0447 (<code>Int<\/code> \u2192 <code>self: Any<\/code>). \u0414\u0435\u0442\u0430\u043b\u0438 \u2014 \u0432 <a href=\"https:\/\/github.com\/jamal-wia\/Paginator\/blob\/master\/docs\/13.%20cursor-pagination.md\" rel=\"noopener noreferrer nofollow\">\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438<\/a>.<\/p>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u2014 \u0432\u0441\u0451 \u043d\u0430 \u043e\u0431\u044b\u0447\u043d\u043e\u043c <code>Paginator<\/code>. \u0411\u0443\u0434\u0435\u043c \u0441\u0447\u0438\u0442\u0430\u0442\u044c, \u0447\u0442\u043e \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 <code>GET \/chats\/:id\/messages?page=N<\/code>.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 0: \u0441\u0435\u0442\u0430\u043f<\/h3>\n<pre><code class=\"kotlin\">class ChatViewModel(    private val api: ChatApi,    private val chatId: String,) : ViewModel() {    private val paginator = mutablePaginator&lt;Message&gt;(capacity = 50) {        load { page -&gt;            val response = api.getMessages(chatId, page)            this.finalPage = response.totalPages  \/\/ \u0443\u0437\u043d\u0430\u0451\u043c \u0433\u0440\u0430\u043d\u0438\u0446\u0443 \u043b\u0435\u043d\u0442\u044b \u0441\u0440\u0430\u0437\u0443 \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435            LoadResult(response.items)        }    }    val uiState = paginator.uiState        .stateIn(viewModelScope, SharingStarted.Eagerly, PaginatorUiState.Idle)    init {        viewModelScope.launch { paginator.restart() }    }    override fun onCleared() {        paginator.release()        super.onCleared()    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0440\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u2014 \u0438 \u0443 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u0441\u0442\u0435\u0439\u0442-\u043c\u0430\u0448\u0438\u043d\u0430 \u0441 <code>Idle \/ Loading \/ Empty \/ Error \/ Content(items, prependState, appendState)<\/code>. \u0412 UI \u044d\u0442\u043e \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432 \u043f\u044f\u0442\u0438\u0441\u0442\u0440\u043e\u0447\u043d\u044b\u0439 <code>when<\/code> \u0438 LazyColumn. \u041f\u0435\u0440\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u0430 \u0434\u043e \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043c\u044b \u0443\u0441\u043f\u0435\u043b\u0438 \u0435\u0451 \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c.<\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 <code>this.finalPage = response.totalPages<\/code> \u0432\u043d\u0443\u0442\u0440\u0438 <code>load<\/code>: \u0440\u0435\u0441\u0438\u0432\u0435\u0440 \u043b\u044f\u043c\u0431\u0434\u044b \u2014 \u0441\u0430\u043c \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u043f\u0440\u0438\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u043c <code>finalPage<\/code> \u043f\u0440\u044f\u043c\u043e \u043d\u0430 \u043c\u0435\u0441\u0442\u0435, \u0431\u0435\u0437 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f <code>uiState<\/code> \u0438 \u0440\u0443\u0447\u043d\u043e\u0439 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438. \u041a\u043e\u0433\u0434\u0430 <code>goNextPage<\/code> \u043f\u043e\u043f\u044b\u0442\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u044b\u0433\u043d\u0443\u0442\u044c \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0443, \u043e\u043d \u0431\u0440\u043e\u0441\u0438\u0442 <code>FinalPageExceededException<\/code>, \u0438 UI \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043f\u043b\u0430\u0448\u043a\u0443 \u00ab\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u043a\u0438\u00bb.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 1: \u0438\u0441\u0442\u043e\u0440\u0438\u044f \u0438 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u0432\u0432\u0435\u0440\u0445<\/h3>\n<p>\u042e\u0437\u0435\u0440 \u043e\u0442\u043a\u0440\u044b\u043b \u0447\u0430\u0442. \u041d\u0443\u0436\u043d\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 50 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439, \u0430 \u043f\u0440\u0438 \u0441\u043a\u0440\u043e\u043b\u043b\u0435 \u0432\u0432\u0435\u0440\u0445 \u2014 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0440\u044b\u0435.<\/p>\n<p>\u0412\u043e\u043f\u0440\u043e\u0441 \u043a <code>Paginator<\/code>: <strong>\u0430 \u0433\u0434\u0435 \u0442\u0443\u0442 \u0432\u0435\u0440\u0445 \u0438 \u0433\u0434\u0435 \u043d\u0438\u0437?<\/strong> \u0423 \u043c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440\u0430 \u043f\u0435\u0440\u0435\u0432\u0451\u0440\u043d\u0443\u0442\u0430\u044f \u043e\u0441\u044c: \u00ab\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 1\u00bb \u2014 \u044d\u0442\u043e \u0441\u0430\u043c\u044b\u0435 \u0441\u0432\u0435\u0436\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u00ab\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 2\u00bb \u2014 \u0441\u0442\u0430\u0440\u0435\u0435. \u0422\u043e \u0435\u0441\u0442\u044c <code>goNextPage<\/code> \u0432 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u00ab\u0433\u0440\u0443\u0437\u0438 \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0440\u0443\u044e \u0438\u0441\u0442\u043e\u0440\u0438\u044e\u00bb.<\/p>\n<pre><code class=\"kotlin\">fun onScrolledToTop() {    viewModelScope.launch { paginator.goNextPage() }}fun onSwipeToRefresh() {    viewModelScope.launch { paginator.restart() }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>goNextPage<\/code> \u0437\u043d\u0430\u0435\u0442, \u0447\u0442\u043e \u0442\u0430\u043a\u043e\u0435 \u00abfilled\u00bb \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 (\u043f\u0440\u0438\u0448\u043b\u043e <code>capacity<\/code> \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432) \u0438 \u00ab\u043d\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u0430\u044f\u00bb (\u043f\u0440\u0438\u0448\u043b\u043e \u043c\u0435\u043d\u044c\u0448\u0435). \u0415\u0441\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u043d\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443, \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0432\u044b\u0437\u043e\u0432 <code>goNextPage<\/code> \u043e\u043d <strong>\u043d\u0435 \u043f\u0435\u0440\u0435\u0441\u043a\u043e\u0447\u0438\u0442 \u0432\u043f\u0435\u0440\u0451\u0434<\/strong>, \u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442 \u0442\u0443 \u0436\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0447\u0435\u0440\u0435\u0437 <code>isFilledSuccessState<\/code> \u2014 \u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439, \u0435\u0441\u043b\u0438 \u0431\u044d\u043a \u0434\u043e\u0441\u043b\u0430\u043b. \u041f\u043e\u0432\u0435\u0440\u0445 \u044d\u0442\u043e\u0433\u043e \u0432 UI \u0443\u0436\u0435 \u0435\u0441\u0442\u044c <code>ProgressPage<\/code> \u0441 \u0440\u0430\u043d\u0435\u0435 \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0442\u0430\u043a \u0447\u0442\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0443\u0432\u0438\u0434\u0438\u0442 \u0441\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0438 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e. \u042d\u0442\u043e \u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438, \u043f\u0438\u0441\u0430\u0442\u044c \u0440\u0443\u043a\u0430\u043c\u0438 \u043d\u0435\u0447\u0435\u0433\u043e.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 2: prefetch \u2014 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u0431\u0435\u0437 \u043a\u043d\u043e\u043f\u043e\u043a \u00ab\u0415\u0449\u0451\u00bb<\/h3>\n<p>\u0420\u0443\u0447\u043d\u043e\u0439 <code>onScrolledToTop<\/code> \u0432 2026 \u0433\u043e\u0434\u0443 \u2014 \u0430\u043d\u0430\u0445\u0440\u043e\u043d\u0438\u0437\u043c. \u0421\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 UX: \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0430\u0442\u044c \u043a\u0430\u0447\u0430\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 <strong>\u0437\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u043a\u0440\u0430\u043d\u043e\u0432 \u0434\u043e \u0442\u043e\u0433\u043e<\/strong>, \u043a\u0430\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442 \u0434\u043e \u043a\u0440\u0430\u044f.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0435\u0441\u0442\u044c <code>PaginatorPrefetchController<\/code> \u2014 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u043e-\u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440, \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0449\u0438\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0438\u0434\u0438\u043c\u044b\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0445 \u0438 \u0441\u0430\u043c \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u0439 <code>goNextPage<\/code> \/ <code>goPreviousPage<\/code>:<\/p>\n<pre><code class=\"kotlin\">private val prefetch = paginator.prefetchController(    scope = viewModelScope,    prefetchDistance = 10,           \/\/ \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u043c \u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0437\u0430 10 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0434\u043e \u043a\u0440\u0430\u044f    enableBackwardPrefetch = true,   \/\/ \u0438 \u0432\u0432\u0435\u0440\u0445 \u0442\u043e\u0436\u0435 (\u0438\u0441\u0442\u043e\u0440\u0438\u044f), \u0438 \u0432\u043d\u0438\u0437 (\u0435\u0441\u043b\u0438 \u0431\u044d\u043a \u043e\u0442\u0434\u0430\u0451\u0442))fun onScroll(firstVisible: Int, lastVisible: Int, total: Int) {    prefetch.onScroll(firstVisible, lastVisible, total)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412 UI \u2014 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435:<\/p>\n<pre><code class=\"kotlin\">val listState = rememberLazyListState()LaunchedEffect(listState) {    snapshotFlow {        Triple(            listState.firstVisibleItemIndex,            listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0,            listState.layoutInfo.totalItemsCount,        )    }.collect { (first, last, total) -&gt; viewModel.onScroll(first, last, total) }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412\u0430\u0436\u043d\u044b\u0435 \u0434\u0435\u0442\u0430\u043b\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0435\u043b\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440:<\/p>\n<ul>\n<li>\n<p><strong>\u041f\u0435\u0440\u0432\u044b\u0439 <\/strong><code><strong>onScroll<\/strong><\/code><strong> \u2014 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043e\u0447\u043d\u044b\u0439.<\/strong> \u041f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0437\u0430\u043f\u043e\u043c\u043d\u0438\u0442 \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u0443\u044e \u043f\u043e\u0437\u0438\u0446\u0438\u044e \u0438 \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043d\u0430\u0447\u043d\u0451\u0442 \u0433\u0440\u0443\u0437\u0438\u0442\u044c \u2014 \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u044b\u043b\u043e \u043b\u043e\u0436\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0438 \u043f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0438 \u044d\u043a\u0440\u0430\u043d\u0430.<\/p>\n<\/li>\n<li>\n<p><strong>\u0422\u0438\u0445\u0430\u044f \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430.<\/strong> \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e <code>silentlyLoading = true<\/code> \u2014 \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e <code>ProgressPage<\/code> \u043d\u0435 \u044d\u043c\u0438\u0442\u0438\u0442\u0441\u044f. UI \u043d\u0435 \u043c\u0438\u0433\u0430\u0435\u0442 \u00abLoading\u00bb \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u043f\u043e\u0434\u043b\u0451\u0442\u0435 \u043a \u043a\u0440\u0430\u044e.<\/p>\n<\/li>\n<li>\n<p><strong>\u0423\u0432\u0430\u0436\u0430\u0435\u0442 <\/strong><code><strong>finalPage<\/strong><\/code><strong>.<\/strong> \u0415\u0441\u043b\u0438 \u0434\u043e\u0448\u043b\u0438 \u0434\u043e \u043a\u043e\u043d\u0446\u0430 \u043b\u0435\u043d\u0442\u044b \u2014 prefetch \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043b\u0438\u0448\u043d\u0438\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u043f\u0443\u0441\u0442\u043e\u0442\u0443 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442.<\/p>\n<\/li>\n<li>\n<p><strong>\u0423\u0432\u0430\u0436\u0430\u0435\u0442 dirty pages.<\/strong> \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0430\u044f-\u0442\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442-\u043e\u043a\u043d\u0435 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u0430 \u043a\u0430\u043a \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0430\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e\u0441\u043b\u0435 \u043e\u0444\u0444\u043b\u0430\u0439\u043d-\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f), \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 prefetch \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442 \u0444\u043e\u043d\u043e\u0432\u043e\u0439 refresh \u044d\u0442\u0438\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p><strong>\u041b\u0435\u0433\u043a\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f.<\/strong> \u041c\u043e\u0434\u0430\u043b\u044c\u043d\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433? <code>prefetch.enabled = false<\/code>, \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u043c\u043e\u043b\u0447\u0438\u0442, \u043f\u043e\u043a\u0430 \u0432\u044b \u0435\u0433\u043e \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u043e\u0431\u0440\u0430\u0442\u043d\u043e.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u043e\u0441\u043b\u0435 <code>jump<\/code> \u0438\u043b\u0438 <code>restart<\/code> \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u2014 \u043d\u0443\u0436\u043d\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0443:<\/p>\n<pre><code class=\"kotlin\">fun openDeeplink(messageId: String) {    viewModelScope.launch {        val location = api.locate(chatId, messageId)        paginator.jump(BookmarkInt(location.page))        prefetch.reset()  \/\/ \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 onScroll \u0441\u0442\u0430\u043d\u0435\u0442 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043e\u0447\u043d\u044b\u043c    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u0441\u0435\u0442\u0430\u043f\u0430 \u043d\u0430 ViewModel, \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 LazyColumn \u2014 \u0438 \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0439 \u0441\u043a\u0440\u043e\u043b\u043b \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u00ab\u0441\u0430\u043c\u00bb. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0441\u0442\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u043d\u0430 Paging 3 \u0431\u0435\u0437 \u0437\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u0445 \u043b\u043e\u0430\u0434\u0435\u0440\u043e\u0432 \u0432 \u0441\u0435\u0440\u0435\u0434\u0438\u043d\u0435 \u0441\u043f\u0438\u0441\u043a\u0430. \u041f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0439\u043c\u0451\u0442.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 3: \u043d\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u0437 WebSocket<\/h3>\n<p>\u041f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u043f\u0443\u0448: <code>{\"type\": \"<\/code><a href=\"http:\/\/message.new\" rel=\"noopener noreferrer nofollow\"><code>message.new<\/code><\/a><code>\", \"message\": {...}}<\/code>. \u041d\u0443\u0436\u043d\u043e \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u0441\u0430\u043c\u044b\u0439 \u0432\u0435\u0440\u0445 (\u0432 \u043d\u0430\u0448\u0435\u0439 \u043e\u0441\u0438 \u2014 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 1, \u0438\u043d\u0434\u0435\u043a\u0441 0), \u043d\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u044f \u043b\u0435\u043d\u0442\u0443.<\/p>\n<pre><code class=\"kotlin\">fun onWebSocketMessage(msg: Message) {    paginator.addAllElements(        elements = listOf(msg),        targetPage = 1,        index = 0,    )}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0427\u0442\u043e \u0442\u0443\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0432\u043d\u0443\u0442\u0440\u0438:<\/p>\n<ol>\n<li>\n<p>\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 page=1 \u043d\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044e 0.<\/p>\n<\/li>\n<li>\n<p>Page=1 \u0443\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 <code>capacity=50<\/code> \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u2014 \u0437\u043d\u0430\u0447\u0438\u0442, \u043f\u043e\u0441\u043b\u0435 \u0432\u0441\u0442\u0430\u0432\u043a\u0438 \u0438\u0445 \u0441\u0442\u0430\u043b\u043e 51. \u041f\u0435\u0440\u0435\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043a\u0430\u0441\u043a\u0430\u0434\u0438\u0440\u0443\u0435\u0442 \u0432\u043f\u0435\u0440\u0451\u0434: \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 page=1 \u0443\u0435\u0437\u0436\u0430\u0435\u0442 \u0432 \u043d\u0430\u0447\u0430\u043b\u043e page=2, \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 page=2 \u2014 \u0432 \u043d\u0430\u0447\u0430\u043b\u043e page=3, \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435 \u043f\u043e \u0446\u0435\u043f\u043e\u0447\u043a\u0435 \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446. \u0418\u043d\u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u00ab\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043d\u0435 \u0431\u043e\u043b\u044c\u0448\u0435 <code>capacity<\/code> \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u00bb \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438.<\/p>\n<\/li>\n<\/ol>\n<p>\u0412\u0441\u0451. \u041e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u043d\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 WebSocket, library \u0441\u0430\u043c\u0430 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0441 capacity invariant. \u0412 Paging 3 \u0442\u0430\u043a\u043e\u0435 \u0434\u0435\u043b\u0430\u043b\u043e\u0441\u044c \u0447\u0435\u0440\u0435\u0437 <code>RemoteMediator<\/code> + \u0440\u0443\u0447\u043d\u0430\u044f \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 Room + <code>invalidate()<\/code> + \u043c\u0435\u0440\u0446\u0430\u043d\u0438\u0435 \u2014 \u0438 \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u043b\u043e\u0441\u044c \u043a\u0440\u0438\u0432\u043e.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 4: optimistic send<\/h3>\n<p>\u042e\u0437\u0435\u0440 \u043d\u0430\u0436\u0430\u043b \u00ab\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c\u00bb. \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e <strong>\u043c\u0433\u043d\u043e\u0432\u0435\u043d\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f<\/strong> \u0432 \u043b\u0435\u043d\u0442\u0435 \u0441 \u043f\u043b\u0430\u0448\u043a\u043e\u0439 \u00ab\u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f\u00bb, \u0430 \u043a\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u0434\u0451\u0442 \u043e\u0442\u0432\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u2014 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0435\u0433\u043e \u043d\u0430 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u043c id. \u0415\u0441\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u043e\u0448\u0438\u0431\u043a\u0443 \u2014 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043b\u0430\u0448\u043a\u0443 \u00ab\u043d\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e\u00bb \u0441 \u043a\u043d\u043e\u043f\u043a\u043e\u0439 \u0440\u0435\u0442\u0440\u0430\u044f.<\/p>\n<p>\u0422\u0443\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0448\u0442\u0443\u043a\u0430, \u043f\u0440\u043e \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0432 \u043f\u0435\u0440\u0432\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0443\u043f\u043e\u043c\u0438\u043d\u0430\u043b \u043c\u0435\u043b\u044c\u043a\u043e\u043c: <code><strong>PageState<\/strong><\/code><strong> \u2014 open-\u0438\u0435\u0440\u0430\u0440\u0445\u0438\u044f<\/strong>. \u041c\u044b \u043c\u043e\u0436\u0435\u043c \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0442\u0438\u043f\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<p>\u0414\u043b\u044f \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u043b\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0430:<\/p>\n<pre><code class=\"kotlin\">data class Message(    val id: String,          \/\/ \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 UUID \u0434\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435    val text: String,    val createdAt: Instant,    val status: MessageStatus = MessageStatus.Sent,)enum class MessageStatus { Sending, Sent, Failed }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u0430\u043c \u043f\u043e\u0442\u043e\u043a \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438:<\/p>\n<pre><code class=\"kotlin\">fun sendMessage(text: String) {    val localId = Uuid.random().toString()    val pending = Message(        id = localId,        text = text,        createdAt = Clock.System.now(),        status = MessageStatus.Sending,    )    \/\/ 1. Optimistic insert    paginator.addAllElements(listOf(pending), targetPage = 1, index = 0, isDirty = true)    viewModelScope.launch {        runCatching { api.send(chatId, text) }            .onSuccess { serverMsg -&gt;                \/\/ 2. \u0417\u0430\u043c\u0435\u043d\u044f\u0435\u043c pending \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435                paginator.updateWhere(                    predicate = { it.id == localId },                    transform = { serverMsg.copy(status = MessageStatus.Sent) },                )            }            .onFailure {                \/\/ 3. \u041f\u043e\u043c\u0435\u0447\u0430\u0435\u043c \u043a\u0430\u043a failed                paginator.updateWhere(                    predicate = { it.id == localId },                    transform = { it.copy(status = MessageStatus.Failed) },                )            }    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>updateWhere<\/code> \u2014 extension \u043d\u0430 <code>MutablePaginator<\/code>, \u043e\u0431\u0445\u043e\u0434\u0438\u0442 \u0432\u0441\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0432 \u043a\u044d\u0448\u0435 \u0438 \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u043f\u0440\u0435\u0434\u0438\u043a\u0430\u0442\u0443. \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0442\u0440\u043e\u043d\u0443\u0442\u044b\u0445. \u0414\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e \u0441\u043b\u0443\u0447\u0430\u044f O(1) \u043f\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u043c (pending \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u0432\u0441\u0442\u0430\u0432\u0438\u043b\u0438 \u0432 page=1, \u043f\u043e\u0438\u0441\u043a \u043d\u0430\u0439\u0434\u0451\u0442 \u0435\u0433\u043e \u0441\u0440\u0430\u0437\u0443), \u043d\u043e \u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u0431\u044b \u0438\u0441\u043a\u0430\u043b\u0438 \u043f\u043e \u0432\u0441\u0435\u043c\u0443 \u0447\u0430\u0442\u0443 \u2014 \u044d\u0442\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u043f\u043e 50 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u043d\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430.<\/p>\n<p>\u041c\u043e\u0436\u043d\u043e \u043f\u043e\u0439\u0442\u0438 \u0434\u0430\u043b\u044c\u0448\u0435 \u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0439 <code>PageState<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 UI \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043b\u0438\u0447\u0430\u0442\u044c \u043e\u0442 \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e Success:<\/p>\n<pre><code class=\"kotlin\">class PendingSendPage&lt;T&gt;(    page: Int,    data: List&lt;T&gt;,    val pendingIds: Set&lt;String&gt;,) : PageState.SuccessPage&lt;T&gt;(page, data)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u043e \u0434\u043b\u044f 90% \u0441\u043b\u0443\u0447\u0430\u0435\u0432 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u043d\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 5: \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435<\/h3>\n<p>\u042e\u0437\u0435\u0440 \u043e\u0442\u043a\u0440\u044b\u043b \u043c\u0435\u043d\u044e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043d\u0430\u0436\u0430\u043b \u00ab\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u00bb. \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u043b\u0438 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440, \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d\u043d\u043e\u0435 \u2014 \u043f\u0430\u0442\u0447\u0438\u043c:<\/p>\n<pre><code class=\"kotlin\">fun editMessage(messageId: String, newText: String) {    viewModelScope.launch {        val updated = api.edit(messageId, newText)        paginator.updateWhere(            predicate = { it.id == messageId },            transform = { updated },        )    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435:<\/p>\n<pre><code class=\"kotlin\">fun deleteMessage(messageId: String) {    viewModelScope.launch {        api.delete(messageId)        paginator.removeAll { it.id == messageId }    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0443\u0442 \u0435\u0441\u0442\u044c \u043a\u0440\u0430\u0441\u0438\u0432\u0430\u044f \u0434\u0435\u0442\u0430\u043b\u044c, \u043e \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u0442\u043e\u0438\u0442 \u0441\u043a\u0430\u0437\u0430\u0442\u044c. \u041a\u043e\u0433\u0434\u0430 \u043c\u044b \u0443\u0434\u0430\u043b\u044f\u0435\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0438\u0437 \u0441\u0435\u0440\u0435\u0434\u0438\u043d\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b, \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f <code>capacity - 1<\/code> \u044d\u043b\u0435\u043c\u0435\u043d\u0442. \u0414\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u0438 <code>goNextPage<\/code> \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442 \u043d\u0430 \u044d\u0442\u0443 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 <code>isFilledSuccessState<\/code> \u0438 \u2014 \u0435\u0441\u043b\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0441\u0442\u0430\u043b\u0430 \u043d\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u043e\u0439 \u2014 \u0434\u043e\u0437\u0430\u0431\u0435\u0440\u0451\u0442 \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u044e\u0449\u0438\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0438\u0437 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. \u0418\u043d\u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u00ab\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043b\u0438\u0431\u043e <code>capacity<\/code> \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u043b\u0438\u0431\u043e \u043c\u044b \u043d\u0430 \u0445\u0432\u043e\u0441\u0442\u0435\u00bb \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438.<\/p>\n<p>\u0412 Paging 3 \u0434\u043b\u044f \u0442\u043e\u0433\u043e \u0436\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u0431\u044b \u043f\u0438\u0441\u0430\u0442\u044c \u0441\u0432\u043e\u0439 <code>RemoteMediator<\/code>, \u0442\u0440\u0438\u0433\u0433\u0435\u0440\u0438\u0442\u044c <code>invalidate()<\/code>, \u043d\u0430\u0434\u0435\u044f\u0442\u044c\u0441\u044f \u043d\u0430 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043a\u0440\u043e\u043b\u043b\u0430. \u0417\u0434\u0435\u0441\u044c \u2014 \u0434\u0432\u0435 \u0441\u0442\u0440\u043e\u043a\u0438.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 6: transaction \u2014 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0430\u0432\u043e\u043a \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e<\/h3>\n<p>\u0411\u044b\u0432\u0430\u0435\u0442 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439, \u043a\u043e\u0433\u0434\u0430 \u043c\u044b \u043c\u0435\u043d\u044f\u0435\u043c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0449\u0435\u0439 \u0441\u0440\u0430\u0437\u0443 \u0438 \u0445\u043e\u0442\u0438\u043c, \u0447\u0442\u043e\u0431\u044b \u043b\u0438\u0431\u043e \u0432\u0441\u0435 \u043e\u043d\u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043b\u0438\u0441\u044c, \u043b\u0438\u0431\u043e \u043d\u0438 \u043e\u0434\u043d\u0430. \u041a\u043b\u0430\u0441\u0441\u0438\u043a\u0430 \u2014 <strong>\u00ab\u041e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u0447\u0430\u0442 \u043a\u0430\u043a \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0439\u00bb<\/strong>: \u0432 \u0441\u043f\u0438\u0441\u043a\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432\u0441\u0435 \u043d\u0435\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0441\u0442\u0430\u0442\u044c \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u043c\u0438, \u0441\u0447\u0451\u0442\u0447\u0438\u043a \u0432 \u0448\u0430\u043f\u043a\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u043e\u0431\u043d\u0443\u043b\u0438\u0442\u044c\u0441\u044f, \u043f\u043b\u0430\u0448\u043a\u0430 \u00abN \u043d\u043e\u0432\u044b\u0445\u00bb \u0434\u043e\u043b\u0436\u043d\u0430 \u0438\u0441\u0447\u0435\u0437\u043d\u0443\u0442\u044c, \u0438 \u0432\u0441\u0451 \u044d\u0442\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c. \u0415\u0441\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0443\u043f\u0430\u0434\u0451\u0442 \u2014 \u043e\u0442\u043a\u0430\u0442\u044b\u0432\u0430\u0435\u043c\u0441\u044f \u043d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 <strong>\u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e<\/strong>, \u0431\u0435\u0437 \u043f\u043e\u043b\u0443\u043c\u0435\u0440.<\/p>\n<p>\u0423 <code>Paginator<\/code> \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0435\u0441\u0442\u044c <code>transaction { }<\/code> \u2014 \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u044b\u0439 \u0431\u043b\u043e\u043a \u0441 deep-copy savepoint \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c. \u0415\u0441\u043b\u0438 \u0432\u043d\u0443\u0442\u0440\u0438 \u0431\u0440\u043e\u0441\u0438\u0442\u0441\u044f \u043b\u044e\u0431\u043e\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 (\u0432\u043a\u043b\u044e\u0447\u0430\u044f <code>CancellationException<\/code>), \u0432\u0441\u0451 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043e\u0442\u043a\u0430\u0442\u0438\u0442\u0441\u044f: \u043a\u044d\u0448, \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442-\u043e\u043a\u043d\u043e, dirty flags, capacity, finalPage, bookmarks, lock-\u0444\u043b\u0430\u0433\u0438. \u0412\u0441\u0451.<\/p>\n<pre><code class=\"kotlin\">fun markChatRead() {    viewModelScope.launch {        try {            paginator.transaction {                \/\/ 1. Optimistic: \u043f\u043e\u043c\u0435\u0447\u0430\u0435\u043c \u0432\u0441\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043a\u0430\u043a \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0435                (this as MutablePaginator).updateAll { msg -&gt;                    if (msg.isRead) msg else msg.copy(isRead = true)                }                \/\/ 2. \u0428\u043b\u0451\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440. \u0415\u0441\u043b\u0438 \u0443\u043f\u0430\u0434\u0451\u0442 \u2014 transaction \u043e\u0442\u043a\u0430\u0442\u0438\u0442 updateAll                api.markChatRead(chatId)            }            \/\/ 3. \u0423\u0441\u043f\u0435\u0445 \u2014 \u0441\u0447\u0451\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0440\u0435\u0430\u0433\u0438\u0440\u043e\u0432\u0430\u043b \u043d\u0430 updateAll \u0447\u0435\u0440\u0435\u0437 uiState        } catch (e: IOException) {            showError(\"\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u043a\u0430\u043a \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0435\")            \/\/ \u0420\u0443\u0447\u043d\u043e\u0439 \u043e\u0442\u043a\u0430\u0442 \u043d\u0435 \u043d\u0443\u0436\u0435\u043d \u2014 transaction \u0443\u0436\u0435 \u0432\u0441\u0451 \u0432\u0435\u0440\u043d\u0443\u043b        }    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0427\u0442\u043e \u0431\u044b\u043b\u043e \u0431\u044b \u0431\u0435\u0437 <code>transaction<\/code>:<\/p>\n<ol>\n<li>\n<p>\u041f\u0438\u0448\u0435\u043c <code>updateAll { ... }<\/code> \u043d\u0430 L1 \u2014 UI \u043e\u0431\u043d\u043e\u0432\u0438\u043b\u0441\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0432\u0438\u043c \u043e\u0448\u0438\u0431\u043a\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0430.<\/p>\n<\/li>\n<li>\n<p><strong>\u0412\u0440\u0443\u0447\u043d\u0443\u044e<\/strong> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043e\u0431\u0440\u0430\u0442\u043d\u043e. \u041d\u043e \u043c\u044b \u0443\u0436\u0435 \u043d\u0435 \u0437\u043d\u0430\u0435\u043c, \u043a\u0430\u043a\u0438\u0435 \u0438\u0437 \u043d\u0438\u0445 \u0431\u044b\u043b\u0438 <code>isRead = false<\/code>, \u0430 \u043a\u0430\u043a\u0438\u0435 <code>isRead = true<\/code> \u0434\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u2014 \u0438\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0437\u0430\u0442\u0451\u0440\u043b\u043e\u0441\u044c.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0451\u0440\u0433\u0430\u0435\u043c <code>refresh<\/code> \u0432\u0441\u0435\u0433\u043e \u0432\u0438\u0434\u0438\u043c\u043e\u0433\u043e \u043e\u043a\u043d\u0430, \u0436\u0434\u0451\u043c \u0441\u0435\u0442\u044c, UI \u043c\u0438\u0433\u0430\u0435\u0442, \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u0438\u0434\u0438\u0442 \u00ab\u043c\u043e\u0440\u0433\u043d\u0443\u0432\u0448\u0438\u0435\u00bb \u043c\u0435\u0442\u043a\u0438 \u043f\u0440\u043e\u0447\u0442\u0435\u043d\u0438\u044f.<\/p>\n<\/li>\n<\/ol>\n<p>\u0421 <code>transaction<\/code> \u043d\u0438\u0447\u0435\u0433\u043e \u044d\u0442\u043e\u0433\u043e \u043d\u0435\u0442: \u043e\u043f\u0442\u0438\u043c\u0438\u0441\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043c\u0433\u043d\u043e\u0432\u0435\u043d\u043d\u043e, \u0438 \u0435\u0441\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u2014 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f <strong>\u0431\u0438\u0442-\u0432-\u0431\u0438\u0442<\/strong> \u043a \u0442\u043e\u043c\u0443, \u043a\u0430\u043a\u0438\u043c \u043e\u043d\u043e \u0431\u044b\u043b\u043e \u0434\u043e \u0431\u043b\u043e\u043a\u0430.<\/p>\n<p>\u0411\u043e\u043b\u0435\u0435 \u0437\u043b\u043e\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u2014 <strong>\u043f\u0435\u0440\u0435\u0441\u044b\u043b\u043a\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u0447\u0430\u0442 \u0441 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c \u0438\u0437 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e<\/strong>:<\/p>\n<pre><code class=\"kotlin\">fun forwardAndDelete(messageIds: List&lt;String&gt;, targetChatId: String) {    viewModelScope.launch {        try {            paginator.transaction {                val mp = this as MutablePaginator&lt;Message&gt;                \/\/ 1. \u041e\u043f\u0442\u0438\u043c\u0438\u0441\u0442\u0438\u0447\u043d\u043e \u0443\u0434\u0430\u043b\u044f\u0435\u043c \u0438\u0437 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0447\u0430\u0442\u0430                val removed = mp.removeAll { it.id in messageIds }                check(removed == messageIds.size) { \"\u043d\u0435 \u0432\u0441\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u043a\u044d\u0448\u0435\" }                \/\/ 2. \u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 (!)                \/\/    jump\/goNext\/refresh \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0431\u0435\u0437 \u0434\u0435\u0434\u043b\u043e\u043a\u0430 \u2014 mutex                \/\/ 3. \u0428\u043b\u0451\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440                api.forward(messageIds, targetChatId)                \/\/ \u0415\u0441\u043b\u0438 forward \u0443\u043f\u0430\u043b \u2014 removeAll \u043e\u0442\u043a\u0430\u0442\u0438\u0442\u0441\u044f, \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432\u0435\u0440\u043d\u0443\u0442\u0441\u044f \u0432 \u043b\u0435\u043d\u0442\u0443            }        } catch (e: Exception) {            showError(\"\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0435\u0440\u0435\u0441\u043b\u0430\u0442\u044c\")        }    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0449\u0451 \u043e\u0434\u043d\u0430 \u043f\u0440\u0438\u044f\u0442\u043d\u0430\u044f \u0434\u0435\u0442\u0430\u043b\u044c: <code>transaction<\/code> \u0432\u043d\u0443\u0442\u0440\u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 <code>flush()<\/code> \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0430 \u0443\u0441\u043f\u0435\u0445\u0435. \u0422\u043e \u0435\u0441\u0442\u044c \u0435\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0451\u043d L2 \u2014 \u0432\u0441\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0438 \u0432\u043d\u0443\u0442\u0440\u0438 \u0431\u043b\u043e\u043a\u0430, \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e \u0437\u0430\u043f\u0438\u0448\u0443\u0442\u0441\u044f \u0432 \u0411\u0414 \u043f\u043e\u0441\u043b\u0435 \u0443\u0441\u043f\u0435\u0445\u0430. \u0415\u0441\u043b\u0438 \u0431\u043b\u043e\u043a \u0443\u043f\u0430\u043b \u2014 L2 \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u043b\u0441\u044f. \u00abEventual consistency\u00bb \u0443\u0440\u043e\u0432\u043d\u044f Room \u0438\u0437 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0438.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 7: deeplink \u0438 \u043f\u0440\u044b\u0436\u043e\u043a \u043d\u0430 \u0437\u0430\u043a\u0440\u0435\u043f<\/h3>\n<p>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0442\u0430\u043f\u043d\u0443\u043b \u043d\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435: \u00ab\u041e\u0442\u0432\u0435\u0442 \u0432 \u0447\u0430\u0442\u0435 X \u043d\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 msg_42\u00bb. \u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u0440\u044b\u043b\u043e\u0441\u044c, \u043d\u0430\u0434\u043e <strong>\u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0447\u0430\u0442, \u0430 \u043f\u0440\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442\u044c \u043a \u043d\u0443\u0436\u043d\u043e\u043c\u0443 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044e<\/strong> \u2014 \u0438 \u0447\u0442\u043e\u0431\u044b \u0432\u043e\u043a\u0440\u0443\u0433 \u043d\u0435\u0433\u043e \u0431\u044b\u043b \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442.<\/p>\n<p>\u0411\u044d\u043a\u0435\u043d\u0434 \u0443\u043c\u0435\u0435\u0442 \u043e\u0442\u0434\u0430\u0432\u0430\u0442\u044c \u00ab\u043d\u0430 \u043a\u0430\u043a\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043b\u0435\u0436\u0438\u0442 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435\u00bb: <code>GET \/chats\/:id\/locate\/:messageId \u2192 {page: 7}<\/code>.<\/p>\n<pre><code class=\"kotlin\">fun openDeeplink(messageId: String) {    viewModelScope.launch {        val location = api.locate(chatId, messageId)        paginator.jump(BookmarkInt(location.page))        prefetch.reset()  \/\/ \u0441\u043f\u0438\u0441\u043e\u043a \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0446\u0435\u043b\u0438\u043a\u043e\u043c \u2014 \u043a\u0430\u043b\u0438\u0431\u0440\u0443\u0435\u043c\u0441\u044f \u0437\u0430\u043d\u043e\u0432\u043e    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 <code>jump<\/code> \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435: \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442-\u043e\u043a\u043d\u043e (<code>startContextPage..endContextPage<\/code>) \u043f\u0435\u0440\u0435\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u043e\u043a\u0440\u0443\u0433 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b 7. \u0421\u043d\u0438\u043c\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 UI, \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b 6, 7, 8 \u2014 \u0442\u043e \u0435\u0441\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c \u00ab\u0434\u043e\u00bb \u0438 \u00ab\u043f\u043e\u0441\u043b\u0435\u00bb. \u0415\u0441\u043b\u0438 \u044e\u0437\u0435\u0440 \u043f\u043e\u0441\u043b\u0435 \u043f\u0440\u044b\u0436\u043a\u0430 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442\u044c \u0432\u0432\u0435\u0440\u0445, <code>goPreviousPage<\/code> \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0433\u0440\u0443\u0436\u0430\u0442\u044c 5, 4, 3 \u2014 \u0438 \u043a\u043e\u0433\u0434\u0430 \u0434\u043e\u0439\u0434\u0451\u0442 \u0434\u043e \u0443\u0436\u0435 \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 (\u0435\u0441\u043b\u0438 \u0440\u0430\u043d\u0435\u0435 \u0431\u044b\u043b \u0441\u043a\u0440\u043e\u043b\u043b \u043e\u0442\u0442\u0443\u0434\u0430) \u2014 <strong>\u043e\u043a\u043d\u0430 \u0441\u043e\u043c\u043a\u043d\u0443\u0442\u0441\u044f \u0431\u0435\u0437 \u0434\u0443\u0431\u043b\u0438\u043a\u0430\u0442\u043e\u0432<\/strong>, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043a\u044d\u0448 \u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043f\u043e <code>page: Int<\/code> \u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 3 \u2014 \u044d\u0442\u043e \u0432\u0441\u0435\u0433\u0434\u0430 \u0442\u0430 \u0436\u0435 \u0441\u0430\u043c\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 3.<\/p>\n<p>\u0414\u043b\u044f <strong>\u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u044b\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439<\/strong> \u043c\u0435\u0445\u0430\u043d\u0438\u043a\u0430 \u0442\u0430 \u0436\u0435, \u043d\u043e \u0441 bookmarks. \u0411\u044d\u043a \u043e\u0442\u0434\u0430\u0451\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u044b\u0445 \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u0438\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u043c\u0438:<\/p>\n<pre><code class=\"kotlin\">viewModelScope.launch {    val pinned = api.getPinned(chatId)  \/\/ List&lt;{messageId, page}&gt;    paginator.bookmarks.clear()    paginator.bookmarks.addAll(pinned.map { BookmarkInt(it.page) })    paginator.recyclingBookmark = true}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0418 \u0432 UI \u2014 \u0434\u0432\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0437\u0430\u043a\u0440\u0435\u043f\u00bb \/ \u00ab\u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u0437\u0430\u043a\u0440\u0435\u043f\u00bb:<\/p>\n<pre><code class=\"kotlin\">fun nextPinned() = viewModelScope.launch { paginator.jumpForward() }fun prevPinned() = viewModelScope.launch { paginator.jumpBack() }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>jumpForward<\/code> \/ <code>jumpBack<\/code> \u0441\u0430\u043c\u0438 \u0441\u043b\u0435\u0434\u044f\u0442, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043f\u0440\u044b\u0433\u0430\u0442\u044c \u043d\u0430 \u0437\u0430\u043a\u0440\u0435\u043f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043d \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435. \u042e\u0437\u0435\u0440 \u043b\u0438\u0441\u0442\u0430\u0435\u0442 \u043c\u0435\u0436\u0434\u0443 \u0437\u0430\u043a\u0440\u0435\u043f\u0430\u043c\u0438, \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u0432\u043e\u043a\u0440\u0443\u0433 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0434\u043e\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u0441\u0430\u043c, \u043e\u043a\u043d\u0430 \u0441\u043c\u044b\u043a\u0430\u044e\u0442\u0441\u044f.<\/p>\n<blockquote>\n<p>\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u0430\u044f \u0441\u043d\u043e\u0441\u043a\u0430: \u0435\u0441\u043b\u0438 \u0432\u0430\u0448 \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 \u043d\u0435 <code>{page: 7}<\/code>, \u0430 \u043a\u0443\u0440\u0441\u043e\u0440 <code>msg_abc123<\/code>, \u2014 \u044d\u0442\u043e \u0442\u043e\u0442 \u0441\u0430\u043c\u044b\u0439 \u0441\u043b\u0443\u0447\u0430\u0439 \u0434\u043b\u044f <code>CursorPaginator<\/code>. \u0422\u0430\u043c \u044d\u0442\u043e <code>jump(CursorBookmark(prev = null, self = \"msg_abc123\", next = null))<\/code>, \u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432 \u043e\u0442\u0432\u0435\u0442\u0435 \u0434\u043e\u0440\u0438\u0441\u0443\u0435\u0442 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0438\u0435 <code>prev<\/code>\/<code>next<\/code>.<\/p>\n<\/blockquote>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 8: date-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 \u0438 \u043f\u043b\u0430\u0448\u043a\u0430 \u00ab\u041d\u043e\u0432\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u00bb<\/h3>\n<p>\u041a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439 UX \u0447\u0430\u0442\u0430: \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u0441\u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e \u0434\u043d\u044f\u043c, \u0441 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0435\u043c \u00ab\u0421\u0435\u0433\u043e\u0434\u043d\u044f\u00bb, \u00ab\u0412\u0447\u0435\u0440\u0430\u00bb, \u00ab17 \u0430\u043f\u0440\u0435\u043b\u044f\u00bb. \u041f\u043b\u044e\u0441 \u0436\u0438\u0440\u043d\u0430\u044f \u043f\u043b\u0430\u0448\u043a\u0430 \u00abN \u043d\u043e\u0432\u044b\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439\u00bb \u043d\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0435 \u043d\u0435\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u043e\u0433\u043e.<\/p>\n<p>\u042d\u0442\u043e <strong>\u043d\u0435 \u0437\u0430\u0434\u0430\u0447\u0430 \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440\u0430<\/strong>. \u041f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u043e\u043f\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u043c\u0438 \u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043c\u0438; \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 \u2014 \u044d\u0442\u043e UI-\u043a\u043e\u043d\u0446\u0435\u043f\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u043b\u0436\u0435\u043d \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043c\u0435\u0436\u0434\u0443 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043c\u0438 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430. \u041d\u043e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u0442 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0447\u0438\u0441\u0442\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u2014 <code>Interweaver<\/code>.<\/p>\n<pre><code class=\"kotlin\">sealed interface ChatRow {    data class Msg(val m: Message) : ChatRow    data class DateSeparator(val day: LocalDate) : ChatRow    data class UnreadBanner(val count: Int) : ChatRow}val chatRows: Flow&lt;List&lt;ChatRow&gt;&gt; = paginator.uiState    .interweave { prev, curr, index -&gt;        buildList {            \/\/ \u041f\u043b\u0430\u0448\u043a\u0430 \u00ab\u041d\u043e\u0432\u044b\u0435\u00bb \u2014 \u043c\u0435\u0436\u0434\u0443 \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u043c\u0438 \u0438 \u043d\u0435\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u043c\u0438            if (prev != null &amp;&amp; prev.isRead &amp;&amp; !curr.isRead) {                add(WovenEntry.Inserted(ChatRow.UnreadBanner(unreadCount)))            }            \/\/ \u0420\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0434\u043d\u044f            val prevDay = prev?.createdAt?.toLocalDate()            val currDay = curr.createdAt.toLocalDate()            if (prevDay != currDay) {                add(WovenEntry.Inserted(ChatRow.DateSeparator(currDay)))            }            add(WovenEntry.Original(ChatRow.Msg(curr)))        }    }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Weaver \u2014 \u044d\u0442\u043e \u0447\u0438\u0441\u0442\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u00ab\u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442, \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u2192 \u0447\u0442\u043e \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044c\u00bb. \u041f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u043f\u0440\u043e \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0437\u043d\u0430\u0435\u0442, UI \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u044b\u0439 \u043f\u043e\u0442\u043e\u043a <code>List&lt;ChatRow&gt;<\/code>. \u041a\u043e\u0433\u0434\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0434\u043e\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u2014 \u043f\u043e\u0442\u043e\u043a \u043f\u0435\u0440\u0435\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 \u0432\u0441\u0442\u0430\u044e\u0442 \u0442\u0443\u0434\u0430, \u043a\u0443\u0434\u0430 \u043d\u0430\u0434\u043e.<\/p>\n<p>\u0412\u0430\u0436\u043d\u043e\u0435: \u044d\u0442\u0430 \u0436\u0435 \u043c\u0435\u0445\u0430\u043d\u0438\u043a\u0430 \u0434\u043e\u0441\u043b\u043e\u0432\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0434\u043b\u044f <code>CursorPaginator<\/code> \u2014 <code>interweave<\/code> \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 <code>PaginatorUiState<\/code>, \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e, \u043a\u0430\u043a \u0430\u0434\u0440\u0435\u0441\u0443\u044e\u0442\u0441\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430 9: \u043e\u0444\u0444\u043b\u0430\u0439\u043d-first<\/h3>\n<p>\u042d\u0442\u043e \u0444\u0438\u043d\u0430\u043b. \u042e\u0437\u0435\u0440 \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0447\u0430\u0442 \u0432 \u043c\u0435\u0442\u0440\u043e \u2014 \u0434\u043e\u043b\u0436\u043d\u043e \u0447\u0442\u043e-\u0442\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f. \u0423\u0431\u0438\u043b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043e\u0442\u043a\u0440\u044b\u043b \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u0447\u0430\u0441\u0430 \u2014 \u0434\u043e\u043b\u0436\u043d\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c\u0441\u044f \u043d\u0430 \u0442\u043e\u043c \u0436\u0435 \u043c\u0435\u0441\u0442\u0435, \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043a\u0440\u043e\u043b\u043b\u043e\u043c. \u0412 \u043e\u0444\u0444\u043b\u0430\u0439\u043d\u0435 \u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u2014 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0432\u0435\u0440\u043d\u0451\u0442\u0441\u044f \u0441\u0435\u0442\u044c. \u0418 \u0432\u0441\u0451 \u044d\u0442\u043e \u2014 \u0431\u0435\u0437 \u043c\u0435\u0440\u0446\u0430\u043d\u0438\u044f UI.<\/p>\n<p>\u042d\u0442\u043e \u0441\u0430\u043c\u0430\u044f \u0431\u043e\u043b\u044c\u0448\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d\u0430 \u043d\u0430 \u0441\u0442\u044b\u043a\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u043e\u0432: L2-\u043a\u044d\u0448, dirty-tracking, process death, warm-up, refresh. \u0420\u0430\u0437\u043b\u043e\u0436\u0438\u043c \u043f\u043e \u0441\u043b\u043e\u044f\u043c.<\/p>\n<h4>9.1. L2-\u043a\u044d\u0448 \u043f\u043e\u0432\u0435\u0440\u0445 Room<\/h4>\n<p>\u0411\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u0430\u043c\u0430 \u043d\u0438\u0447\u0435\u0433\u043e \u0432 \u0411\u0414 \u043d\u0435 \u043f\u0438\u0448\u0435\u0442 \u2014 \u043e\u043d\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 <code>PersistentPagingCache&lt;T&gt;<\/code> \u0441 \u043f\u044f\u0442\u044c\u044e \u043c\u0435\u0442\u043e\u0434\u0430\u043c\u0438: <code>save<\/code>, <code>load<\/code>, <code>loadAll<\/code>, <code>remove<\/code>, <code>clear<\/code>. \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u2014 \u043d\u0430 \u0432\u0430\u0448\u0435\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435. \u0428\u0430\u0431\u043b\u043e\u043d\u043d\u044b\u0439 Room-\u0431\u044d\u043a\u0435\u043d\u0434 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"kotlin\">@Entity(tableName = \"messages_pages\")data class PageEntity(    @PrimaryKey val page: Int,    val chatId: String,    val dataJson: String,    val isEmpty: Boolean,    val updatedAt: Long,)@Daointerface PageDao {    @Upsert suspend fun upsert(entity: PageEntity)    @Query(\"SELECT * FROM messages_pages WHERE chatId = :chatId AND page = :page\")    suspend fun get(chatId: String, page: Int): PageEntity?    @Query(\"SELECT * FROM messages_pages WHERE chatId = :chatId ORDER BY page\")    suspend fun getAll(chatId: String): List&lt;PageEntity&gt;    @Query(\"DELETE FROM messages_pages WHERE chatId = :chatId AND page = :page\")    suspend fun delete(chatId: String, page: Int)    @Query(\"DELETE FROM messages_pages WHERE chatId = :chatId\")    suspend fun clear(chatId: String)}class RoomMessagesCache(    private val dao: PageDao,    private val chatId: String,) : PersistentPagingCache&lt;Message&gt; {    private val serializer = ListSerializer(Message.serializer())    override suspend fun save(state: PageState&lt;Message&gt;) {        dao.upsert(            PageEntity(                page = state.page,                chatId = chatId,                dataJson = Json.encodeToString(serializer, state.data),                isEmpty = state.isEmptyState(),                updatedAt = Clock.System.now().toEpochMilliseconds(),            )        )    }    override suspend fun load(page: Int): PageState&lt;Message&gt;? {        val entity = dao.get(chatId, page) ?: return null        val data = Json.decodeFromString(serializer, entity.dataJson)        return if (entity.isEmpty) EmptyPage(page, data)        else SuccessPage(page, data.toMutableList())    }    override suspend fun loadAll(): List&lt;PageState&lt;Message&gt;&gt; =        dao.getAll(chatId).mapNotNull { load(it.page) }    override suspend fun remove(page: Int) = dao.delete(chatId, page)    override suspend fun clear() = dao.clear(chatId)}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u043c \u0432 DSL:<\/p>\n<pre><code class=\"kotlin\">private val paginator = mutablePaginator&lt;Message&gt;(capacity = 50) {    load { page -&gt;        val response = api.getMessages(chatId, page)        this.finalPage = response.totalPages        LoadResult(response.items)    }    cache = LruPagingCache(maxSize = 20)   \/\/ L1: \u0434\u0435\u0440\u0436\u0438\u043c \u0432 \u043f\u0430\u043c\u044f\u0442\u0438 20 \u0441\u0442\u0440\u0430\u043d\u0438\u0446    persistentCache = RoomMessagesCache(dao, chatId)  \/\/ L2: \u0432\u0441\u0451}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0418 \u0432\u0441\u0451. \u0414\u0430\u043b\u044c\u0448\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438:<\/p>\n<ul>\n<li>\n<p><strong>Read-path<\/strong>: L1 \u2192 L2 \u2192 network. \u041d\u0430 cache-miss \u0432 \u043f\u0430\u043c\u044f\u0442\u0438 \u2014 \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0437\u0430\u0433\u043b\u044f\u0434\u044b\u0432\u0430\u0435\u0442 \u0432 Room, \u0438 \u0435\u0441\u043b\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0442\u0430\u043c \u0435\u0441\u0442\u044c, \u043e\u043d\u0430 \u043f\u0440\u043e\u043c\u043e\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0432 L1 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u043c\u0433\u043d\u043e\u0432\u0435\u043d\u043d\u043e. \u0421\u0435\u0442\u044c \u043d\u0435 \u0434\u0451\u0440\u0433\u0430\u0435\u0442\u0441\u044f, \u043b\u043e\u0430\u0434\u0435\u0440 \u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f.<\/p>\n<\/li>\n<li>\n<p><strong>Write-path<\/strong>: \u043f\u043e\u0441\u043b\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0433\u043e <code>load<\/code> \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0432 L2. \u0422\u043e \u0435\u0441\u0442\u044c \u0432\u0441\u0451, \u0447\u0442\u043e \u044e\u0437\u0435\u0440 \u0432\u0438\u0434\u0435\u043b \u0445\u043e\u0442\u044f \u0431\u044b \u0440\u0430\u0437, \u2014 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043e.<\/p>\n<\/li>\n<\/ul>\n<h4>9.2. Warm-up \u043d\u0430 \u0445\u043e\u043b\u043e\u0434\u043d\u043e\u043c \u0441\u0442\u0430\u0440\u0442\u0435<\/h4>\n<p>\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e L2 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f <strong>\u043b\u0435\u043d\u0438\u0432\u043e<\/strong> \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440\u0443 \u043d\u0443\u0436\u043d\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430. \u041d\u043e \u0434\u043b\u044f \u0447\u0430\u0442\u0430 \u044d\u0442\u043e \u043d\u0435 \u0442\u043e, \u0447\u0442\u043e \u043c\u044b \u0445\u043e\u0442\u0438\u043c. \u041c\u044b \u0445\u043e\u0442\u0438\u043c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 \u043e\u0444\u0444\u043b\u0430\u0439\u043d\u0435 <strong>\u0432\u0441\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u0430\u044f \u043b\u0435\u043d\u0442\u0430<\/strong> \u0431\u044b\u043b\u0430 \u0441\u0440\u0430\u0437\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430, \u0431\u0435\u0437 \u00abLoading&#8230;\u00bb.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0435\u0441\u0442\u044c <code>warmUpFromPersistent()<\/code>:<\/p>\n<pre><code class=\"kotlin\">init {    viewModelScope.launch {        val inserted = paginator.warmUpFromPersistent()        if (inserted == 0) {            \/\/ \u041a\u044d\u0448 \u043f\u0443\u0441\u0442 \u2014 \u044d\u0442\u043e \u043f\u0435\u0440\u0432\u044b\u0439 \u0437\u0430\u0445\u043e\u0434 \u0432 \u0447\u0430\u0442. \u0422\u044f\u043d\u0435\u043c \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430.            paginator.restart()        } else {            \/\/ \u0415\u0441\u0442\u044c \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435. \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u043d\u0435\u043c\u0435\u0434\u043b\u0435\u043d\u043d\u043e, \u0432 \u0444\u043e\u043d\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c.            paginator.refresh(pages = paginator.core.affectedPages.toList())        }    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>warmUpFromPersistent<\/code> \u0432\u0435\u0440\u043d\u0451\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u0438 \u0442\u0438\u0445\u043e (\u0431\u0435\u0437 \u044d\u043c\u0438\u0442\u0430 snapshot) \u0440\u0430\u0437\u043b\u043e\u0436\u0438\u0442 \u0438\u0445 \u043f\u043e L1. \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 <code>jump\/goNextPage<\/code> \u043f\u043e\u043f\u0430\u0434\u0451\u0442 \u0441\u0440\u0430\u0437\u0443 \u0432 L1, \u0431\u0435\u0437 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430.<\/p>\n<p>\u041d\u044e\u0430\u043d\u0441: \u0435\u0441\u043b\u0438 \u0443 \u043d\u0430\u0441 <code>LruPagingCache(maxSize = 20)<\/code>, \u0430 \u0432 Room \u043b\u0435\u0436\u0438\u0442 100 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u2014 \u0432 L1 \u043f\u043e\u043f\u0430\u0434\u0443\u0442 \u0442\u043e\u043b\u044c\u043a\u043e 20 (\u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u043e\u0433\u0440\u0435\u0432 \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 <code>setState<\/code>). \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 80 \u043e\u0441\u0442\u0430\u043d\u0443\u0442\u0441\u044f \u0432 L2 \u0438 \u043f\u043e\u0434\u0442\u044f\u043d\u0443\u0442\u0441\u044f \u043f\u043e \u043c\u0435\u0440\u0435 \u0441\u043a\u0440\u043e\u043b\u043b\u0430.<\/p>\n<h4>9.3. Process death: SavedStateHandle<\/h4>\n<p>Android \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0431\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0432 \u043b\u044e\u0431\u043e\u0439 \u043c\u043e\u043c\u0435\u043d\u0442. L2 \u044d\u0442\u043e, \u043a\u043e\u043d\u0435\u0447\u043d\u043e, \u043f\u0435\u0440\u0435\u0436\u0438\u0432\u0451\u0442 \u2014 \u043d\u043e <strong>\u043f\u043e\u0437\u0438\u0446\u0438\u044f \u0441\u043a\u0440\u043e\u043b\u043b\u0430, \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442-\u043e\u043a\u043d\u043e, bookmarks, lock-\u0444\u043b\u0430\u0433\u0438<\/strong> \u0436\u0438\u0432\u0443\u0442 \u0432 \u043f\u0430\u043c\u044f\u0442\u0438 \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440\u0430. \u041d\u0443\u0436\u043d\u043e \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0435\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0446\u0435\u043b\u0438\u043a\u043e\u043c.<\/p>\n<pre><code class=\"kotlin\">class ChatViewModel(    private val api: ChatApi,    private val chatId: String,    private val savedState: SavedStateHandle,) : ViewModel() {    private val paginator = mutablePaginator&lt;Message&gt;(capacity = 50) {        load { page -&gt;            val response = api.getMessages(chatId, page)            this.finalPage = response.totalPages            LoadResult(response.items)        }        persistentCache = RoomMessagesCache(dao, chatId)    }    init {        viewModelScope.launch {            \/\/ 1. \u041f\u0440\u043e\u0431\u0443\u0435\u043c \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043d\u0438\u043c\u043e\u043a \u0438\u0437 SavedStateHandle (process death)            val snapshot: String? = savedState[SNAPSHOT_KEY]            if (snapshot != null) {                paginator.restoreStateFromJson(snapshot, Message.serializer())            } else {                \/\/ 2. \u041f\u0440\u043e\u0431\u0443\u0435\u043c \u043f\u0440\u043e\u0433\u0440\u0435\u0442\u044c \u0438\u0437 Room (cold start)                val inserted = paginator.warmUpFromPersistent()                if (inserted == 0) paginator.restart()            }        }        \/\/ \u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u0441\u043d\u0438\u043c\u043e\u043a \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 \u0447\u0442\u043e-\u0442\u043e \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f        paginator.uiState            .debounce(500)            .onEach {                savedState[SNAPSHOT_KEY] = paginator.saveStateToJson(                    elementSerializer = Message.serializer(),                    contextOnly = true,   \/\/ \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0438\u0434\u0438\u043c\u044b\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b                )            }            .launchIn(viewModelScope)    }    companion object {        private const val SNAPSHOT_KEY = \"chat_paginator_snapshot\"    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>contextOnly = true<\/code> \u2014 \u043a\u043b\u044e\u0447\u0435\u0432\u0430\u044f \u0434\u0435\u0442\u0430\u043b\u044c. \u0411\u0435\u0437 \u043d\u0435\u0451 \u043c\u044b \u0431\u044b \u0441\u0435\u0440\u0438\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438 \u0432\u0435\u0441\u044c \u043a\u044d\u0448 (\u043f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e \u0441\u043e\u0442\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446), \u0438 Bundle \u043c\u043e\u0433 \u0431\u044b \u043f\u0440\u0435\u0432\u044b\u0441\u0438\u0442\u044c \u043b\u0438\u043c\u0438\u0442 TransactionTooLargeException (1MB). \u0421 <code>contextOnly = true<\/code> \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u043e\u043a\u043d\u0430 \u2014 \u043e\u0431\u044b\u0447\u043d\u043e 3-5 \u0448\u0442\u0443\u043a, \u0441\u043e\u0442\u043d\u044f \u043a\u0438\u043b\u043e\u0431\u0430\u0439\u0442 JSON, \u0432\u043b\u0435\u0437\u0430\u0435\u0442 \u0431\u0435\u0437 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.<\/p>\n<p>\u041f\u0440\u0438 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438:<\/p>\n<ul>\n<li>\n<p><code>ErrorPage<\/code> \u0438 <code>ProgressPage<\/code> \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0432 <code>SuccessPage<\/code> \/ <code>EmptyPage<\/code> \u0438 \u043f\u043e\u043c\u0435\u0447\u0430\u044e\u0442\u0441\u044f dirty \u2014 \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u0436\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u0435 \u043a \u043d\u0438\u043c \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0438\u0445 \u043e\u0431\u043d\u043e\u0432\u0438\u043b.<\/p>\n<\/li>\n<li>\n<p>\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442-\u043e\u043a\u043d\u043e, bookmarks, lock-\u0444\u043b\u0430\u0433\u0438, <code>finalPage<\/code> \u2014 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u0435\u0441\u0442\u044c.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u043e\u0441\u043b\u0435 <code>restoreStateFromJson<\/code> \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a, \u043a\u0430\u043a \u0431\u0443\u0434\u0442\u043e process death \u043d\u0435 \u0431\u044b\u043b\u043e \u2014 \u0442\u043e\u0442 \u0436\u0435 \u0441\u043a\u0440\u043e\u043b\u043b, \u0442\u043e\u0442 \u0436\u0435 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442.<\/p>\n<h4>9.4. Dirty-tracking \u0438 \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u0430\u044f \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f<\/h4>\n<p>\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u0430\u043c\u043e\u0435 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e\u0435. \u042e\u0437\u0435\u0440 \u0432 \u043e\u0444\u0444\u043b\u0430\u0439\u043d\u0435:<\/p>\n<ol>\n<li>\n<p>\u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u2014 <code>updateWhere<\/code> \u0441 <code>isDirty = true<\/code> \u043d\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435.<\/p>\n<\/li>\n<li>\n<p>\u0423\u0434\u0430\u043b\u0438\u043b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u2014 <code>removeAll<\/code> \u0441 <code>isDirty = true<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043d\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u2014 <code>addAllElements(... isDirty = true)<\/code>.<\/p>\n<\/li>\n<\/ol>\n<p>\u0412\u0441\u0435 \u044d\u0442\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043b\u0435\u0436\u0430\u0442 \u0432 L1. \u0418\u0445 \u043d\u0443\u0436\u043d\u043e:<\/p>\n<ul>\n<li>\n<p><strong>\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432 L2<\/strong>, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438 \u0443\u0431\u0438\u0439\u0441\u0442\u0432\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043e\u043d\u0438 \u043d\u0435 \u043f\u043e\u0442\u0435\u0440\u044f\u043b\u0438\u0441\u044c.<\/p>\n<\/li>\n<li>\n<p><strong>\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440<\/strong>, \u043a\u043e\u0433\u0434\u0430 \u0432\u0435\u0440\u043d\u0451\u0442\u0441\u044f \u0441\u0435\u0442\u044c.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u043b\u044f L2 \u2014 <code>flush()<\/code>:<\/p>\n<pre><code class=\"kotlin\">\/\/ \u041f\u043e\u0441\u043b\u0435 \u043f\u0430\u0447\u043a\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u2014 \u044f\u0432\u043d\u044b\u0439 flushpaginator.flush()<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041b\u0438\u0431\u043e \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u2014 \u0432\u043d\u0443\u0442\u0440\u0438 <code>transaction { }<\/code> flush \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u0430\u043c \u043d\u0430 \u0443\u0441\u043f\u0435\u0445\u0435.<\/p>\n<p><code>MutablePaginator<\/code> \u0441\u0430\u043c \u0442\u0440\u0435\u043a\u0430\u0435\u0442 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f: <code>affectedPages: Set&lt;Int&gt;<\/code> \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442, \u043a\u0430\u043a\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0431\u044b\u043b\u0438 \u0442\u0440\u043e\u043d\u0443\u0442\u044b, <code>hasPendingFlush: Boolean<\/code> \u2014 \u0435\u0441\u0442\u044c \u043b\u0438 \u0432\u043e\u043e\u0431\u0449\u0435 \u0447\u0442\u043e-\u0442\u043e \u043d\u0435\u0437\u0430\u0441\u0435\u0439\u0432\u043b\u0435\u043d\u043d\u043e\u0435. \u042d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0434\u043b\u044f UI-\u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u00ab\u043d\u0435\u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u00bb \u0438\u043b\u0438 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432.<\/p>\n<p>\u0414\u043b\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f (\u043c\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u043c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0437\u043d\u0430\u0442\u044c, \u043a\u0430\u043a\u043e\u0439 API \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u00ab\u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u00bb), \u043d\u043e \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0432\u0441\u0451, \u0447\u0442\u043e\u0431\u044b \u0435\u0433\u043e \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u044c:<\/p>\n<pre><code class=\"kotlin\">fun onNetworkAvailable() {    viewModelScope.launch {        \/\/ 1. \u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0441\u043e \u0441\u0432\u043e\u0438\u043c REST-\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c        outboxSyncer.syncAll()  \/\/ \u0432\u0430\u0448 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0439 \u043a\u043e\u0434        \/\/ 2. \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0432\u0438\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u2014 \u0432\u0434\u0440\u0443\u0433 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043f\u0440\u0438\u043b\u0435\u0442\u0435\u043b\u043e \u0447\u0442\u043e-\u0442\u043e \u043d\u043e\u0432\u043e\u0435        val visiblePages = paginator.core.run { startContextPage..endContextPage }.toList()        paginator.refresh(visiblePages)        \/\/ 3. \u041d\u0430 \u0432\u0441\u044f\u043a\u0438\u0439 \u0441\u043b\u0443\u0447\u0430\u0439 \u2014 flush L1 \u0432 L2        paginator.flush()    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>9.5. \u0427\u0442\u043e \u0432 \u0438\u0442\u043e\u0433\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442<\/h4>\n<p>\u0421\u043e\u0431\u0435\u0440\u0451\u043c \u0432 \u043e\u0434\u043d\u0443 \u043a\u0430\u0440\u0442\u0438\u043d\u0443:<\/p>\n<ul>\n<li>\n<p><strong>\u042e\u0437\u0435\u0440 \u0435\u0434\u0435\u0442 \u0432 \u043c\u0435\u0442\u0440\u043e<\/strong> \u2192 \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0447\u0430\u0442. \u0422\u0443\u0442 \u0436\u0435 \u0432\u0438\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 20 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u2014 prefetch \u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0435\u0442 \u0435\u0449\u0451 \u0438\u0437 L2 \u043f\u043e \u043c\u0435\u0440\u0435 \u0441\u043a\u0440\u043e\u043b\u043b\u0430.<\/p>\n<\/li>\n<li>\n<p><strong>\u041d\u0430\u043f\u0438\u0441\u0430\u043b \u0447\u0442\u043e-\u0442\u043e<\/strong> \u2192 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043e \u0432 L1 \u0441 <code>isDirty = true<\/code>, \u043b\u0435\u0436\u0438\u0442 \u0432 \u043f\u0430\u043c\u044f\u0442\u0438.<\/p>\n<\/li>\n<li>\n<p><strong>\u0423\u0431\u0438\u043b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435<\/strong> \u2192 <code>SavedStateHandle<\/code> \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u043b \u0441\u043d\u0438\u043c\u043e\u043a \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u043e\u043a\u043d\u0430.<\/p>\n<\/li>\n<li>\n<p><strong>\u041e\u0442\u043a\u0440\u044b\u043b \u0447\u0435\u0440\u0435\u0437 \u0447\u0430\u0441<\/strong> \u2192 <code>restoreStateFromJson<\/code> \u043f\u043e\u0434\u043d\u044f\u043b \u043e\u043a\u043d\u043e \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043a\u0440\u043e\u043b\u043b\u043e\u043c. \u0412\u0441\u0451 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u2014 \u0438\u0437 L2.<\/p>\n<\/li>\n<li>\n<p><strong>\u041f\u043e\u044f\u0432\u0438\u043b\u0430\u0441\u044c \u0441\u0435\u0442\u044c<\/strong> \u2192 <code>outboxSyncer.syncAll()<\/code> \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f, <code>refresh<\/code> \u043e\u0431\u043d\u043e\u0432\u0438\u043b \u0432\u0438\u0434\u0438\u043c\u043e\u0435 \u043e\u043a\u043d\u043e, <code>flush<\/code> \u0437\u0430\u043f\u0438\u0441\u0430\u043b \u0438\u0442\u043e\u0433 \u0432 L2.<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e <code>invalidate()<\/code>. \u041d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e <code>Flow&lt;PagingData&gt;<\/code>. \u041d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u043c\u0435\u0440\u0446\u0430\u043d\u0438\u044f.<\/p>\n<h3>\u0427\u0442\u043e \u0443 \u043d\u0430\u0441 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c<\/h3>\n<p>\u041e\u0434\u0438\u043d ViewModel. \u0414\u0435\u0432\u044f\u0442\u044c \u0431\u043e\u0435\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043e\u0431\u0435\u0440\u0451\u043c:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">\u0417\u0430\u0434\u0430\u0447\u0430<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u0412\u044b\u0437\u043e\u0432<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u0421\u0442\u0440\u043e\u043a<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0438 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u0432\u0432\u0435\u0440\u0445<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>goNextPage()<\/code>, <code>restart()<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">2<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Prefetch<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>prefetchController(...)<\/code> + <code>onScroll(...)<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">~10<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041d\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u0437 WebSocket<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>addAllElements(..., targetPage = 1)<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">1<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Optimistic send<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>addAllElements<\/code> + <code>updateWhere<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">~15<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \/ \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>updateWhere<\/code>, <code>removeAll<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">2<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Transaction<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>transaction { updateAll + <\/code><a href=\"http:\/\/api.call\" rel=\"noopener noreferrer nofollow\"><code>api.call<\/code><\/a><code>() }<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">~10<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Deeplink + \u0437\u0430\u043a\u0440\u0435\u043f\u044b<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>jump(BookmarkInt)<\/code>, <code>bookmarks<\/code>, <code>jumpForward<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">~8<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Date-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 + \u00ab\u041d\u043e\u0432\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u00bb<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>uiState.interweave { ... }<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">~15<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041e\u0444\u0444\u043b\u0430\u0439\u043d-first + process death<\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>warmUpFromPersistent<\/code>, <code>saveStateToJson<\/code>, <code>flush<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">~40<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p><strong>\u041d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e <\/strong><code><strong>RemoteMediator<\/strong><\/code><strong>. \u041d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e <\/strong><code><strong>PagingSource<\/strong><\/code><strong>. \u041d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e <\/strong><code><strong>invalidate()<\/strong><\/code><strong>.<\/strong><\/p>\n<p>\u0418 \u0441\u0430\u043c\u043e\u0435 \u043f\u0440\u0438\u044f\u0442\u043d\u043e\u0435 \u2014 \u044d\u0442\u043e \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u0439 Kotlin Multiplatform \u043a\u043e\u0434. \u0422\u043e\u0442 \u0436\u0435 ViewModel \u043a\u043e\u043c\u043f\u0438\u043b\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0434 iOS, \u0438 \u0442\u0430\u043c <code>uiState<\/code> \u0442\u0430\u043a \u0436\u0435 \u043f\u043e\u0434\u0446\u0435\u043f\u0438\u0442\u0441\u044f \u043a SwiftUI \u0447\u0435\u0440\u0435\u0437 \u0442\u043e\u043d\u043a\u0438\u0439 \u0430\u0434\u0430\u043f\u0442\u0435\u0440. Paging 3 \u043d\u0430 \u044d\u0442\u043e\u043c \u043c\u043e\u043c\u0435\u043d\u0442\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0432\u044b\u0445\u043e\u0434\u0438\u0442 \u0438\u0437 \u0447\u0430\u0442\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0435\u0433\u043e \u043d\u0435\u0442 \u0432\u043d\u0435 Android.<\/p>\n<p>\u0410 \u0435\u0441\u043b\u0438 \u0432\u0430\u0448 \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 \u043a\u0443\u0440\u0441\u043e\u0440\u044b \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u043e\u043c\u0435\u0440\u043e\u0432 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u2014 \u0432\u0441\u0451 \u0440\u043e\u0432\u043d\u043e \u0442\u043e \u0436\u0435 \u0441\u0430\u043c\u043e\u0435, \u0442\u043e\u043b\u044c\u043a\u043e <code>Paginator<\/code> \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430 <code>CursorPaginator<\/code>, <code>BookmarkInt(N)<\/code> \u2014 \u043d\u0430 <code>CursorBookmark(prev, self, next)<\/code>, <code>targetPage = 1<\/code> \u2014 \u043d\u0430 <code>targetSelf = headCursor<\/code>. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0434\u043e\u0441\u043b\u043e\u0432\u043d\u043e.<\/p>\n<hr\/>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u2014 \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043c, <strong>\u043a\u0430\u043a \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0438\u0437\u043d\u0443\u0442\u0440\u0438<\/strong>: \u0442\u0440\u0438 \u0441\u043b\u043e\u044f (<code>PagingCore<\/code> \/ <code>Paginator<\/code> \/ <code>MutablePaginator<\/code>), mutex \u0432\u043c\u0435\u0441\u0442\u043e \u0433\u043e\u043d\u043e\u043a, \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0441 savepoint \u0434\u043b\u044f \u043e\u0442\u043a\u0430\u0442\u0430, \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 <code>PageState<\/code> \u2014 sealed, \u043d\u043e \u0432\u0441\u0435 \u0435\u0433\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u043d\u0438\u043a\u0438 <code>open<\/code>. \u042d\u0442\u043e \u0434\u043b\u044f \u0442\u0435\u0445, \u043a\u0442\u043e \u043b\u044e\u0431\u0438\u0442 \u0447\u0438\u0442\u0430\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e API, \u043d\u043e \u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<p><strong>\u0415\u0441\u043b\u0438 \u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u043e\u0441\u044c \u2014 \u0437\u0432\u0435\u0437\u0434\u0430 \u043d\u0430 <\/strong><a href=\"https:\/\/github.com\/jamal-wia\/Paginator\" rel=\"noopener noreferrer nofollow\"><strong>GitHub<\/strong><\/a><strong> \u0441\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442, \u0441\u043f\u0430\u0441\u0438\u0431\u043e.<\/strong><\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1027686\/\">https:\/\/habr.com\/ru\/articles\/1027686\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0412 \u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043b Paginator \u0441 Paging 3 \u043d\u0430 \u043a\u043e\u0448\u0430\u0447\u044c\u0435\u043c \u0443\u0440\u043e\u0432\u043d\u0435: \u00ab\u0432\u043e\u0442 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0444\u0438\u0434, \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u2014 \u0442\u0440\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u043c\u0435\u0441\u0442\u043e \u0442\u0440\u0438\u0434\u0446\u0430\u0442\u0438\u00bb. \u042d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0434\u043b\u044f \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0437\u043d\u0430\u043a\u043e\u043c\u0441\u0442\u0432\u0430, \u043d\u043e \u043d\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043d\u0430 \u0433\u043b\u0430\u0432\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441: \u0430 \u043a\u0430\u043a \u043e\u043d\u043e \u0441\u0435\u0431\u044f \u043f\u043e\u0432\u0435\u0434\u0451\u0442, \u043a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0442 \u043d\u0430\u0447\u043d\u0451\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0442\u043e, \u0440\u0430\u0434\u0438 \u0447\u0435\u0433\u043e \u043b\u044e\u0434\u0438 \u043e\u0431\u044b\u0447\u043d\u043e \u0438 \u043f\u0438\u0448\u0443\u0442 \u0441\u0432\u043e\u0439 \u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434 \u043f\u043e\u0432\u0435\u0440\u0445 Paging 3?\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0431\u0435\u0440\u0443 \u043c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440 \u2014 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440 \u044d\u0442\u043e \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u0433\u043e\u043d. \u0422\u0430\u043c \u0435\u0441\u0442\u044c:\u043b\u0435\u043d\u0442\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0441 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u043e\u0439 \u0432\u0432\u0435\u0440\u0445 \u0438 \u0432\u043d\u0438\u0437,\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u043d\u0430 \u0441\u043a\u0440\u043e\u043b\u043b\u0435 (prefetch) \u0431\u0435\u0437 \u043a\u043d\u043e\u043f\u043e\u043a \u00ab\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0449\u0451\u00bb,\u043d\u043e\u0432\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0438\u0437 WebSocket \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438,optimistic send \u0441 \u043e\u0442\u043a\u0430\u0442\u043e\u043c \u043f\u0440\u0438 \u043e\u0448\u0438\u0431\u043a\u0435,\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435,deeplink \u043d\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438 \u043f\u0440\u044b\u0436\u043a\u0438 \u043d\u0430 \u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u044b\u0435,date-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 \u0438 \u043f\u043b\u0430\u0448\u043a\u0430 \u00ab\u041d\u043e\u0432\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u00bb,\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u043a\u0438 (\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0430\u0442\u043e\u043c\u0430\u0440\u043d\u043e, \u0441 \u043e\u0442\u043a\u0430\u0442\u043e\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435),\u0440\u0430\u0431\u043e\u0442\u0430 \u043e\u0444\u0444\u043b\u0430\u0439\u043d \u0441 \u043f\u0435\u0440\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435\u043c process death.\u0414\u0435\u0432\u044f\u0442\u044c \u0431\u043e\u0435\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447. \u041e\u0434\u043d\u0430 ViewModel. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u043a\u043e\u0441\u0442\u044b\u043b\u0435\u0439.\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440 \u043f\u0440\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u0443\u044e \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044e\u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0447\u043d\u0451\u043c: \u0435\u0441\u043b\u0438 \u0432\u0430\u0448 \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043d\u0435 \u043f\u043e \u043d\u043e\u043c\u0435\u0440\u0443 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b, \u0430 \u043f\u043e nextCursor \/ prevCursor (GraphQL connections, Slack API, Instagram, Reddit \u0438 \u043f\u0440\u043e\u0447\u0438\u0435 \u043b\u0435\u043d\u0442\u044b \u0441 \u00ab\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u044b\u043c \u043a\u0440\u0430\u0435\u043c\u00bb), \u2014 \u0432\u0430\u043c \u043d\u0443\u0436\u0435\u043d \u043d\u0435 Paginator, \u0430 \u0435\u0433\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u044b\u0439 \u0431\u0440\u0430\u0442 CursorPaginator.\u042d\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u044b \u0438 Int-\u0438\u043d\u0434\u0435\u043a\u0441\u044b \u0436\u0438\u0432\u0443\u0442 \u043f\u043e \u0440\u0430\u0437\u043d\u044b\u043c \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c: \u0443 \u043a\u0443\u0440\u0441\u043e\u0440\u0430 \u043d\u0435\u0442 \u00ab\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b 42\u00bb, \u043d\u0435\u0442 random-access \u043f\u0440\u044b\u0436\u043a\u043e\u0432 \u043d\u0430 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440, \u043d\u0435\u0442 resize(capacity). \u0417\u0430\u0442\u043e \u0435\u0441\u0442\u044c CursorBookmark(prev, self, next) \u0438 LinkedList-\u043c\u043e\u0434\u0435\u043b\u044c, \u0433\u0434\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u043d\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u0432\u043e\u0438\u0445 \u0441\u043e\u0441\u0435\u0434\u0435\u0439.API \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u2014 \u0437\u0435\u0440\u043a\u0430\u043b\u044c\u043d\u043e\u0435:val paginator = mutableCursorPaginator&lt;Message&gt;(capacity = 50) {    load { cursor -&gt;        val page = api.getMessages(cursor?.self as? String)        CursorLoadResult(            data = page.items,            bookmark = CursorBookmark(                prev = page.prevCursor,                self = page.selfCursor,                next = page.nextCursor,            ),        )    }}\u0422\u0435 \u0436\u0435 uiState, jump, goNextPage, interweave, transaction, L2-\u043a\u044d\u0448 \u2014 \u0432\u0441\u0451 \u043d\u0430 \u043c\u0435\u0441\u0442\u0435. \u041f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0438\u0437 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u044f\u0442\u0441\u044f \u043e\u0434\u0438\u043d-\u0432-\u043e\u0434\u0438\u043d, \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u044e\u0447 (Int \u2192 self: Any). \u0414\u0435\u0442\u0430\u043b\u0438 \u2014 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438.\u0414\u0430\u043b\u044c\u0448\u0435 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u2014 \u0432\u0441\u0451 \u043d\u0430 \u043e\u0431\u044b\u0447\u043d\u043e\u043c Paginator. \u0411\u0443\u0434\u0435\u043c \u0441\u0447\u0438\u0442\u0430\u0442\u044c, \u0447\u0442\u043e \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 GET \/chats\/:id\/messages?page=N.\u0417\u0430\u0434\u0430\u0447\u0430 0: \u0441\u0435\u0442\u0430\u043fclass ChatViewModel(    private val api: ChatApi,    private val chatId: String,) : ViewModel() {    private val paginator = mutablePaginator&lt;Message&gt;(capacity = 50) {        load { page -&gt;            val response = api.getMessages(chatId, page)            this.finalPage = response.totalPages  \/\/ \u0443\u0437\u043d\u0430\u0451\u043c \u0433\u0440\u0430\u043d\u0438\u0446\u0443 \u043b\u0435\u043d\u0442\u044b \u0441\u0440\u0430\u0437\u0443 \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435            LoadResult(response.items)        }    }    val uiState = paginator.uiState        .stateIn(viewModelScope, SharingStarted.Eagerly, PaginatorUiState.Idle)    init {        viewModelScope.launch { paginator.restart() }    }    override fun onCleared() {        paginator.release()        super.onCleared()    }}\u0422\u0440\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u2014 \u0438 \u0443 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u0441\u0442\u0435\u0439\u0442-\u043c\u0430\u0448\u0438\u043d\u0430 \u0441 Idle \/ Loading \/ Empty \/ Error \/ Content(items, prependState, appendState). \u0412 UI \u044d\u0442\u043e \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432 \u043f\u044f\u0442\u0438\u0441\u0442\u0440\u043e\u0447\u043d\u044b\u0439 when \u0438 LazyColumn. \u041f\u0435\u0440\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u0430 \u0434\u043e \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043c\u044b \u0443\u0441\u043f\u0435\u043b\u0438 \u0435\u0451 \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c.\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 this.finalPage = response.totalPages \u0432\u043d\u0443\u0442\u0440\u0438 load: \u0440\u0435\u0441\u0438\u0432\u0435\u0440 \u043b\u044f\u043c\u0431\u0434\u044b \u2014 \u0441\u0430\u043c \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u043f\u0440\u0438\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u043c finalPage \u043f\u0440\u044f\u043c\u043e \u043d\u0430 \u043c\u0435\u0441\u0442\u0435, \u0431\u0435\u0437 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f uiState \u0438 \u0440\u0443\u0447\u043d\u043e\u0439 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438. \u041a\u043e\u0433\u0434\u0430 goNextPage \u043f\u043e\u043f\u044b\u0442\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u044b\u0433\u043d\u0443\u0442\u044c \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0443, \u043e\u043d \u0431\u0440\u043e\u0441\u0438\u0442 FinalPageExceededException, \u0438 UI \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043f\u043b\u0430\u0448\u043a\u0443 \u00ab\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u043a\u0438\u00bb.\u0417\u0430\u0434\u0430\u0447\u0430 1: \u0438\u0441\u0442\u043e\u0440\u0438\u044f \u0438 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u0432\u0432\u0435\u0440\u0445\u042e\u0437\u0435\u0440 \u043e\u0442\u043a\u0440\u044b\u043b \u0447\u0430\u0442. \u041d\u0443\u0436\u043d\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 50 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439, \u0430 \u043f\u0440\u0438 \u0441\u043a\u0440\u043e\u043b\u043b\u0435 \u0432\u0432\u0435\u0440\u0445 \u2014 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0440\u044b\u0435.\u0412\u043e\u043f\u0440\u043e\u0441 \u043a Paginator: \u0430 \u0433\u0434\u0435 \u0442\u0443\u0442 \u0432\u0435\u0440\u0445 \u0438 \u0433\u0434\u0435 \u043d\u0438\u0437? \u0423 \u043c\u0435\u0441\u0441\u0435\u043d\u0434\u0436\u0435\u0440\u0430 \u043f\u0435\u0440\u0435\u0432\u0451\u0440\u043d\u0443\u0442\u0430\u044f \u043e\u0441\u044c: \u00ab\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 1\u00bb \u2014 \u044d\u0442\u043e \u0441\u0430\u043c\u044b\u0435 \u0441\u0432\u0435\u0436\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u00ab\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 2\u00bb \u2014 \u0441\u0442\u0430\u0440\u0435\u0435. \u0422\u043e \u0435\u0441\u0442\u044c goNextPage \u0432 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u00ab\u0433\u0440\u0443\u0437\u0438 \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0440\u0443\u044e \u0438\u0441\u0442\u043e\u0440\u0438\u044e\u00bb.fun onScrolledToTop() {    viewModelScope.launch { paginator.goNextPage() }}fun onSwipeToRefresh() {    viewModelScope.launch { paginator.restart() }}goNextPage \u0437\u043d\u0430\u0435\u0442, \u0447\u0442\u043e \u0442\u0430\u043a\u043e\u0435 \u00abfilled\u00bb \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 (\u043f\u0440\u0438\u0448\u043b\u043e capacity \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432) \u0438 \u00ab\u043d\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u0430\u044f\u00bb (\u043f\u0440\u0438\u0448\u043b\u043e \u043c\u0435\u043d\u044c\u0448\u0435). \u0415\u0441\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u043d\u0435\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443, \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0432\u044b\u0437\u043e\u0432 goNextPage \u043e\u043d \u043d\u0435 \u043f\u0435\u0440\u0435\u0441\u043a\u043e\u0447\u0438\u0442 \u0432\u043f\u0435\u0440\u0451\u0434, \u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442 \u0442\u0443 \u0436\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0447\u0435\u0440\u0435\u0437 isFilledSuccessState \u2014 \u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439, \u0435\u0441\u043b\u0438 \u0431\u044d\u043a \u0434\u043e\u0441\u043b\u0430\u043b. \u041f\u043e\u0432\u0435\u0440\u0445 \u044d\u0442\u043e\u0433\u043e \u0432 UI \u0443\u0436\u0435 \u0435\u0441\u0442\u044c ProgressPage \u0441 \u0440\u0430\u043d\u0435\u0435 \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0442\u0430\u043a \u0447\u0442\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0443\u0432\u0438\u0434\u0438\u0442 \u0441\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0438 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e. \u042d\u0442\u043e \u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438, \u043f\u0438\u0441\u0430\u0442\u044c \u0440\u0443\u043a\u0430\u043c\u0438 \u043d\u0435\u0447\u0435\u0433\u043e.\u0417\u0430\u0434\u0430\u0447\u0430 2: prefetch \u2014 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430 \u0431\u0435\u0437 \u043a\u043d\u043e\u043f\u043e\u043a \u00ab\u0415\u0449\u0451\u00bb\u0420\u0443\u0447\u043d\u043e\u0439 onScrolledToTop \u0432 2026 \u0433\u043e\u0434\u0443 \u2014 \u0430\u043d\u0430\u0445\u0440\u043e\u043d\u0438\u0437\u043c. \u0421\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 UX: \u043f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0430\u0442\u044c \u043a\u0430\u0447\u0430\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0437\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u043a\u0440\u0430\u043d\u043e\u0432 \u0434\u043e \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442 \u0434\u043e \u043a\u0440\u0430\u044f.\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0435\u0441\u0442\u044c PaginatorPrefetchController \u2014 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u043e-\u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440, \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0449\u0438\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0438\u0434\u0438\u043c\u044b\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0445 \u0438 \u0441\u0430\u043c \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u0439 goNextPage \/ goPreviousPage:private val prefetch = paginator.prefetchController(    scope = viewModelScope,    prefetchDistance = 10,           \/\/ \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u043c \u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0437\u0430 10 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0434\u043e \u043a\u0440\u0430\u044f    enableBackwardPrefetch = true,   \/\/ \u0438 \u0432\u0432\u0435\u0440\u0445 \u0442\u043e\u0436\u0435 (\u0438\u0441\u0442\u043e\u0440\u0438\u044f), \u0438 \u0432\u043d\u0438\u0437 (\u0435\u0441\u043b\u0438 \u0431\u044d\u043a \u043e\u0442\u0434\u0430\u0451\u0442))fun onScroll(firstVisible: Int, lastVisible: Int, total: Int) {    prefetch.onScroll(firstVisible, lastVisible, total)}\u0412 UI \u2014 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435:val listState = rememberLazyListState()LaunchedEffect(listState) {    snapshotFlow {        Triple(            listState.firstVisibleItemIndex,            listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0,            listState.layoutInfo.totalItemsCount,        )    }.collect { (first, last, total) -&gt; viewModel.onScroll(first, last, total) }}\u0412\u0430\u0436\u043d\u044b\u0435 \u0434\u0435\u0442\u0430\u043b\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0435\u043b\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440:\u041f\u0435\u0440\u0432\u044b\u0439 onScroll \u2014 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043e\u0447\u043d\u044b\u0439. \u041f\u0430\u0433\u0438\u043d\u0430\u0442\u043e\u0440 \u0437\u0430\u043f\u043e\u043c\u043d\u0438\u0442 \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u0443\u044e \u043f\u043e\u0437\u0438\u0446\u0438\u044e \u0438 \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043d\u0430\u0447\u043d\u0451\u0442 \u0433\u0440\u0443\u0437\u0438\u0442\u044c \u2014 \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u044b\u043b\u043e \u043b\u043e\u0436\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0438 \u043f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0438 \u044d\u043a\u0440\u0430\u043d\u0430.\u0422\u0438\u0445\u0430\u044f \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0430. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e silentlyLoading = true \u2014 \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e ProgressPage \u043d\u0435 \u044d\u043c\u0438\u0442\u0438\u0442\u0441\u044f. UI \u043d\u0435 \u043c\u0438\u0433\u0430\u0435\u0442 \u00abLoading\u00bb \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u043f\u043e\u0434\u043b\u0451\u0442\u0435 \u043a \u043a\u0440\u0430\u044e.\u0423\u0432\u0430\u0436\u0430\u0435\u0442 finalPage. \u0415\u0441\u043b\u0438 \u0434\u043e\u0448\u043b\u0438 \u0434\u043e \u043a\u043e\u043d\u0446\u0430 \u043b\u0435\u043d\u0442\u044b \u2014 prefetch \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043b\u0438\u0448\u043d\u0438\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u043f\u0443\u0441\u0442\u043e\u0442\u0443 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442.\u0423\u0432\u0430\u0436\u0430\u0435\u0442 dirty pages. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0430\u044f-\u0442\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442-\u043e\u043a\u043d\u0435 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u0430 \u043a\u0430\u043a \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0430\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e\u0441\u043b\u0435 \u043e\u0444\u0444\u043b\u0430\u0439\u043d-\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f), \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 prefetch \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442 \u0444\u043e\u043d\u043e\u0432\u043e\u0439 refresh \u044d\u0442\u0438\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e.\u041b\u0435\u0433\u043a\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f. \u041c\u043e\u0434\u0430\u043b\u044c\u043d\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433? prefetch.enabled = false, \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u043c\u043e\u043b\u0447\u0438\u0442, \u043f\u043e\u043a\u0430 \u0432\u044b \u0435\u0433\u043e \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u043e\u0431\u0440\u0430\u0442\u043d\u043e.\u041f\u043e\u0441\u043b\u0435 jump \u0438\u043b\u0438 restart \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u2014 \u043d\u0443\u0436\u043d\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0443:fun openDeeplink(messageId: String) {    viewModelScope.launch {        val location = api.locate(chatId, messageId)        paginator.jump(BookmarkInt(location.page))        prefetch.reset()  \/\/ \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 onScroll \u0441\u0442\u0430\u043d\u0435\u0442 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043e\u0447\u043d\u044b\u043c    }}\u041e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u0441\u0435\u0442\u0430\u043f\u0430 \u043d\u0430 ViewModel, \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 LazyColumn \u2014 \u0438 \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0439 \u0441\u043a\u0440\u043e\u043b\u043b \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u00ab\u0441\u0430\u043c\u00bb. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0441\u0442\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u043d\u0430 Paging 3 \u0431\u0435\u0437 \u0437\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u0445 \u043b\u043e\u0430\u0434\u0435\u0440\u043e\u0432 \u0432 \u0441\u0435\u0440\u0435\u0434\u0438\u043d\u0435 \u0441\u043f\u0438\u0441\u043a\u0430. \u041f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0439\u043c\u0451\u0442.\u0417\u0430\u0434\u0430\u0447\u0430 3: \u043d\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u0437 WebSocket\u041f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u043f\u0443\u0448: {&#171;type&#187;: &#171;message.new&#187;, &#171;message&#187;: {&#8230;}}. \u041d\u0443\u0436\u043d\u043e \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u0441\u0430\u043c\u044b\u0439 \u0432\u0435\u0440\u0445 (\u0432 \u043d\u0430\u0448\u0435\u0439 \u043e\u0441\u0438 \u2014 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 1, \u0438\u043d\u0434\u0435\u043a\u0441 0), \u043d\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u044f \u043b\u0435\u043d\u0442\u0443.fun onWebSocketMessage(msg: Message) {    paginator.addAllElements(        elements = listOf(msg),        targetPage = 1,        index = 0,    )}\u0427\u0442\u043e \u0442\u0443\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0432\u043d\u0443\u0442\u0440\u0438:\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 page=1 \u043d\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044e 0.Page=1 \u0443\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 capacity=50 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u2014 \u0437\u043d\u0430\u0447\u0438\u0442, \u043f\u043e\u0441\u043b\u0435 \u0432\u0441\u0442\u0430\u0432\u043a\u0438 \u0438\u0445 \u0441\u0442\u0430\u043b\u043e 51. \u041f\u0435\u0440\u0435\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043a\u0430\u0441\u043a\u0430\u0434\u0438\u0440\u0443\u0435\u0442 \u0432\u043f\u0435\u0440\u0451\u0434: \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 page=1 \u0443\u0435\u0437\u0436\u0430\u0435\u0442 \u0432 \u043d\u0430\u0447\u0430\u043b\u043e page=2, \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 page=2 \u2014 \u0432 \u043d\u0430\u0447\u0430\u043b\u043e page=3, \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435 \u043f\u043e \u0446\u0435\u043f\u043e\u0447\u043a\u0435 \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446. \u0418\u043d\u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u00ab\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043d\u0435 \u0431\u043e\u043b\u044c\u0448\u0435 capacity \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u00bb \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438.\u0412\u0441\u0451. \u041e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u043d\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 WebSocket, library \u0441\u0430\u043c\u0430 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0441 capacity invariant. \u0412 Paging 3 \u0442\u0430\u043a\u043e\u0435 \u0434\u0435\u043b\u0430\u043b\u043e\u0441\u044c \u0447\u0435\u0440\u0435\u0437 RemoteMediator + \u0440\u0443\u0447\u043d\u0430\u044f \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 Room + invalidate() + \u043c\u0435\u0440\u0446\u0430\u043d\u0438\u0435 \u2014 \u0438 \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u043b\u043e\u0441\u044c \u043a\u0440\u0438\u0432\u043e.\u0417\u0430\u0434\u0430\u0447\u0430 4: optimistic send\u042e\u0437\u0435\u0440 \u043d\u0430\u0436\u0430\u043b \u00ab\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c\u00bb. \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043c\u0433\u043d\u043e\u0432\u0435\u043d\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432 \u043b\u0435\u043d\u0442\u0435 \u0441 \u043f\u043b\u0430\u0448\u043a\u043e\u0439 \u00ab\u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f\u00bb, \u0430 \u043a\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u0434\u0451\u0442 \u043e\u0442\u0432\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u2014 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0435\u0433\u043e \u043d\u0430 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u043c id. \u0415\u0441\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u043e\u0448\u0438\u0431\u043a\u0443 \u2014 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043b\u0430\u0448\u043a\u0443 \u00ab\u043d\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e\u00bb \u0441 \u043a\u043d\u043e\u043f\u043a\u043e\u0439 \u0440\u0435\u0442\u0440\u0430\u044f.\u0422\u0443\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0448\u0442\u0443\u043a\u0430, \u043f\u0440\u043e \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0432 \u043f\u0435\u0440\u0432\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0443\u043f\u043e\u043c\u0438\u043d\u0430\u043b \u043c\u0435\u043b\u044c\u043a\u043e\u043c: PageState \u2014 open-\u0438\u0435\u0440\u0430\u0440\u0445\u0438\u044f. \u041c\u044b \u043c\u043e\u0436\u0435\u043c \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0442\u0438\u043f\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432.\u0414\u043b\u044f \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u043b\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0430:data class Message(    val id: String,          \/\/ \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 UUID \u0434\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435    val text: String,    val createdAt: Instant,    val status: MessageStatus = MessageStatus.Sent,)enum class MessageStatus { Sending, Sent, Failed }\u0421\u0430\u043c \u043f\u043e\u0442\u043e\u043a \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438:fun sendMessage(text: String) {    val localId = Uuid.random().toString()    val pending = Message(        id = localId,        text = text,        createdAt = Clock.System.now(),        status = MessageStatus.Sending,    )    \/\/ 1. Optimistic insert    paginator.addAllElements(listOf(pending), targetPage = 1, index = 0, isDirty = true)    viewModelScope.launch {        runCatching { api.send(chatId, text) }            .onSuccess { serverMsg -&gt;                \/\/ 2. \u0417\u0430\u043c\u0435\u043d\u044f\u0435\u043c pending \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435                paginator.updateWhere(                    predicate = { it.id == localId },                    transform = { serverMsg.copy(status = MessageStatus.Sent) },                )            }            .onFailure {                \/\/ 3. \u041f\u043e\u043c\u0435\u0447\u0430\u0435\u043c \u043a\u0430\u043a failed                paginator.updateWhere(                    predicate = { it.id == localId },                    transform = { it.copy(status = MessageStatus.Failed) },                )            }    }}updateWhere \u2014 extension \u043d\u0430 MutablePaginator, \u043e\u0431\u0445\u043e\u0434\u0438\u0442 \u0432\u0441\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0432 \u043a\u044d\u0448\u0435 \u0438 \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u043f\u0440\u0435\u0434\u0438\u043a\u0430\u0442\u0443. \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0442\u0440\u043e\u043d\u0443\u0442\u044b\u0445. \u0414\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e \u0441\u043b\u0443\u0447\u0430\u044f O(1) \u043f\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u043c (pending \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u0432\u0441\u0442\u0430\u0432\u0438\u043b\u0438 \u0432 page=1, \u043f\u043e\u0438\u0441\u043a \u043d\u0430\u0439\u0434\u0451\u0442 \u0435\u0433\u043e \u0441\u0440\u0430\u0437\u0443), \u043d\u043e \u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u0431\u044b \u0438\u0441\u043a\u0430\u043b\u0438 \u043f\u043e \u0432\u0441\u0435\u043c\u0443 \u0447\u0430\u0442\u0443 \u2014 \u044d\u0442\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u043f\u043e 50 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u043d\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430.\u041c\u043e\u0436\u043d\u043e \u043f\u043e\u0439\u0442\u0438 \u0434\u0430\u043b\u044c\u0448\u0435 \u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0439 PageState, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 UI \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043b\u0438\u0447\u0430\u0442\u044c \u043e\u0442 \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e Success:class PendingSendPage&lt;T&gt;(&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-477312","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/477312","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=477312"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/477312\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=477312"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=477312"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=477312"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}