1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.paging
18 
19 import androidx.annotation.IntRange
20 import androidx.paging.PagingSource.LoadResult.Page
21 
22 /**
23  * Snapshot state of Paging system including the loaded [pages], the last accessed [anchorPosition],
24  * and the [config] used.
25  */
26 public class PagingState<Key : Any, Value : Any>
27 constructor(
28     /** Loaded pages of data in the list. */
29     public val pages: List<Page<Key, Value>>,
30     /**
31      * Most recently accessed index in the list, including placeholders.
32      *
33      * `null` if no access in the [PagingData] has been made yet. E.g., if this snapshot was
34      * generated before or during the first load.
35      */
36     public val anchorPosition: Int?,
37     /** [PagingConfig] that was given when initializing the [PagingData] stream. */
38     public val config: PagingConfig,
39     /**
40      * Number of placeholders before the first loaded item if placeholders are enabled, otherwise 0.
41      */
42     @IntRange(from = 0) private val leadingPlaceholderCount: Int
43 ) {
44 
45     override fun equals(other: Any?): Boolean {
46         return other is PagingState<*, *> &&
47             pages == other.pages &&
48             anchorPosition == other.anchorPosition &&
49             config == other.config &&
50             leadingPlaceholderCount == other.leadingPlaceholderCount
51     }
52 
53     override fun hashCode(): Int {
54         return pages.hashCode() +
55             anchorPosition.hashCode() +
56             config.hashCode() +
57             leadingPlaceholderCount.hashCode()
58     }
59 
60     /**
61      * Coerces [anchorPosition] to closest loaded value in [pages].
62      *
63      * This function can be called with [anchorPosition] to fetch the loaded item that is closest to
64      * the last accessed index in the list.
65      *
66      * @param anchorPosition Index in the list, including placeholders.
67      * @return The closest loaded [Value] in [pages] to the provided [anchorPosition]. `null` if all
68      *   loaded [pages] are empty.
69      */
70     public fun closestItemToPosition(anchorPosition: Int): Value? {
71         if (pages.all { it.data.isEmpty() }) return null
72 
73         anchorPositionToPagedIndices(anchorPosition) { pageIndex, index ->
74             val firstNonEmptyPage = pages.first { it.data.isNotEmpty() }
75             val lastNonEmptyPage = pages.last { it.data.isNotEmpty() }
76             return when {
77                 index < 0 -> firstNonEmptyPage.data.first()
78                 pageIndex == pages.lastIndex && index > pages.last().data.lastIndex -> {
79                     lastNonEmptyPage.data.last()
80                 }
81                 else -> pages[pageIndex].data[index]
82             }
83         }
84     }
85 
86     /**
87      * Coerces an index in the list, including placeholders, to closest loaded page in [pages].
88      *
89      * This function can be called with [anchorPosition] to fetch the loaded page that is closest to
90      * the last accessed index in the list.
91      *
92      * @param anchorPosition Index in the list, including placeholders.
93      * @return The closest loaded [Value] in [pages] to the provided [anchorPosition]. `null` if all
94      *   loaded [pages] are empty.
95      */
96     public fun closestPageToPosition(anchorPosition: Int): Page<Key, Value>? {
97         if (pages.all { it.data.isEmpty() }) return null
98 
99         anchorPositionToPagedIndices(anchorPosition) { pageIndex, index ->
100             return when {
101                 index < 0 -> pages.first()
102                 else -> pages[pageIndex]
103             }
104         }
105     }
106 
107     /**
108      * @return `true` if all loaded pages are empty or no pages were loaded when this [PagingState]
109      *   was created, `false` otherwise.
110      */
111     public fun isEmpty(): Boolean = pages.all { it.data.isEmpty() }
112 
113     /**
114      * @return The first loaded item in the list or `null` if all loaded pages are empty or no pages
115      *   were loaded when this [PagingState] was created.
116      */
117     public fun firstItemOrNull(): Value? {
118         return pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
119     }
120 
121     /**
122      * @return The last loaded item in the list or `null` if all loaded pages are empty or no pages
123      *   were loaded when this [PagingState] was created.
124      */
125     public fun lastItemOrNull(): Value? {
126         return pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
127     }
128 
129     override fun toString(): String {
130         return "PagingState(pages=$pages, anchorPosition=$anchorPosition, config=$config, " +
131             "leadingPlaceholderCount=$leadingPlaceholderCount)"
132     }
133 
134     internal inline fun <T> anchorPositionToPagedIndices(
135         anchorPosition: Int,
136         block: (pageIndex: Int, index: Int) -> T
137     ): T {
138         var pageIndex = 0
139         var index = anchorPosition - leadingPlaceholderCount
140         while (pageIndex < pages.lastIndex && index > pages[pageIndex].data.lastIndex) {
141             index -= pages[pageIndex].data.size
142             pageIndex++
143         }
144 
145         return block(pageIndex, index)
146     }
147 }
148