├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .idea
└── copyright
│ ├── Apache.xml
│ └── profiles_settings.xml
├── LICENSE.txt
├── benchmarks
├── benchmarkable
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── benchmarks
│ │ │ └── BenchmarkRunTest.kt
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── benchmarks
│ │ │ └── data
│ │ │ ├── Benchmarked.kt
│ │ │ ├── Paging.kt
│ │ │ └── Tiling.kt
│ │ └── test
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── benchmarks
│ │ └── ExampleUnitTest.kt
└── microbenchmark
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── microbenchmark
│ │ └── AllocationBenchmark.kt
│ └── main
│ └── AndroidManifest.xml
├── build-logic
├── convention
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ ├── android-app-library-convention.kt
│ │ ├── android-application-convention.gradle.kts
│ │ ├── android-library-convention.gradle.kts
│ │ ├── kotlin-jvm-convention.gradle.kts
│ │ ├── kotlin-jvm-convention.kt
│ │ ├── kotlin-library-convention.gradle.kts
│ │ └── publishing-library-convention.gradle.kts
├── settings.gradle.kts
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── build.gradle.kts
├── contributing.md
├── docs
├── assets
│ └── logo.png
├── images
│ ├── adaptive.gif
│ ├── basic.gif
│ ├── complex.gif
│ ├── placeholders.gif
│ ├── search.gif
│ └── sticky.gif
├── implementation
│ ├── pagination-types.md
│ ├── performance.md
│ ├── pivoted-tiling.md
│ ├── primitives.md
│ └── tiledlist.md
├── index.md
└── usecases
│ ├── adaptive-paging.md
│ ├── basic-example.md
│ ├── complex-tiling.md
│ ├── compose.md
│ ├── overview.md
│ ├── placeholders.md
│ ├── search.md
│ └── transformations.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── library
├── compose
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── tunjid
│ │ └── tiler
│ │ └── compose
│ │ └── Effects.kt
└── tiler
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── tunjid
│ │ └── tiler
│ │ ├── ConcurrentListTiler.kt
│ │ ├── Pivoting.kt
│ │ ├── Tile.kt
│ │ ├── TiledList.kt
│ │ ├── TiledListExt.kt
│ │ ├── Tiler.kt
│ │ └── utilities
│ │ ├── ChunkedTiledList.kt
│ │ ├── EmptyTiledList.kt
│ │ ├── IntArrayList.kt
│ │ ├── NeighbouredQueryFetcher.kt
│ │ ├── SparseQueryArray.kt
│ │ └── SparseTiledList.kt
│ └── commonTest
│ └── kotlin
│ └── com
│ └── tunjid
│ ├── tiler
│ ├── TileKtTest.kt
│ ├── TiledListKtTest.kt
│ ├── TilerKtTest.kt
│ ├── TilingIterationOrderTest.kt
│ └── Utilities.kt
│ └── utilities
│ ├── ChunkedTiledListTest.kt
│ ├── IntArrayListTest.kt
│ ├── NeighboredQueryFetcherTest.kt
│ ├── PivotingKtTest.kt
│ └── SparseTiledListTest.kt
├── libraryVersion.properties
├── misc
└── demo.gif
├── mkdocs.yml
├── readme.md
├── sample
├── android
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── tunjid
│ │ │ └── tyler
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
├── browser
│ ├── build.gradle.kts
│ ├── resources
│ │ ├── index.html
│ │ └── styles.css
│ └── src
│ │ └── jsMain
│ │ ├── kotlin
│ │ └── main.js.kt
│ │ └── resources
│ │ ├── index.html
│ │ └── styles.css
├── common
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ └── values
│ │ │ └── strings.xml
│ │ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── tunjid
│ │ │ └── demo
│ │ │ └── common
│ │ │ └── ui
│ │ │ ├── Root.kt
│ │ │ ├── Theme.kt
│ │ │ └── numbers
│ │ │ ├── AdaptiveTiledGrid.kt
│ │ │ ├── Colors.kt
│ │ │ ├── Loader.kt
│ │ │ ├── NumberTile.kt
│ │ │ ├── NumberUtilities.kt
│ │ │ └── StickyHeaderTiledList.kt
│ │ └── iosMain
│ │ └── kotlin
│ │ └── main.ios.kt
├── desktop
│ ├── build.gradle.kts
│ └── src
│ │ └── jvmMain
│ │ └── kotlin
│ │ └── com
│ │ └── tunjid
│ │ └── demo
│ │ └── Main.kt
└── ios
│ ├── tiler.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── adetunji_dahunsi.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── adetunji_dahunsi.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── tiler
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── tilerApp.swift
└── settings.gradle.kts
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: JVM Tests
2 |
3 | on:
4 | push:
5 | branches: [ develop ]
6 | pull_request:
7 | branches: [ develop ]
8 |
9 | jobs:
10 | test:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: checkout
16 | uses: actions/checkout@v2
17 | - name: Set up JDK
18 | uses: actions/setup-java@v2
19 | with:
20 | java-version: '17'
21 | distribution: 'adopt'
22 | - name: Validate Gradle wrapper
23 | uses: gradle/actions/wrapper-validation@v3
24 | - name: JVM tests
25 | run: ./gradlew jvmTest
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/copyright/Apache.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("org.jetbrains.kotlin.android")
3 | id("android-library-convention")
4 | }
5 |
6 |
7 | kotlin {
8 | jvmToolchain(17)
9 | }
10 | android {
11 | namespace = "com.example.benchmarks"
12 | buildFeatures {
13 | compose = false
14 | }
15 | defaultConfig {
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | testInstrumentationRunnerArguments["androidx.benchmark.profiling.mode"] = "StackSampling"
18 | testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR,LOW-BATTERY"
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation(project(":library:tiler"))
24 | implementation(libs.androidx.paging)
25 | implementation(libs.kotlinx.coroutines.core)
26 |
27 | testImplementation(libs.kotlinx.coroutines.test)
28 | testImplementation(libs.androidx.test.core)
29 | testImplementation(libs.androidx.test.junit)
30 | testImplementation(libs.androidx.test.rules)
31 | testImplementation(libs.androidx.test.runner)
32 |
33 | androidTestImplementation(libs.kotlinx.coroutines.test)
34 | androidTestImplementation(libs.androidx.test.core)
35 | androidTestImplementation(libs.androidx.test.junit)
36 | androidTestImplementation(libs.androidx.test.rules)
37 | androidTestImplementation(libs.androidx.test.runner)
38 |
39 | }
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/benchmarks/benchmarkable/consumer-rules.pro
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/src/androidTest/java/com/example/benchmarks/BenchmarkRunTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.benchmarks
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.example.benchmarks.data.PAGE_TO_SCROLL_TO
5 | import com.example.benchmarks.data.PagingBenchmark
6 | import com.example.benchmarks.data.TilingBenchmark
7 | import com.example.benchmarks.data.emptyPages
8 | import com.example.benchmarks.data.offScreenPages
9 | import com.example.benchmarks.data.onScreenPages
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.Assert.*
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 |
15 | /**
16 | * Instrumented test, which will execute on an Android device.
17 | *
18 | * See [testing documentation](http://d.android.com/tools/testing).
19 | */
20 | @RunWith(AndroidJUnit4::class)
21 | class BenchmarkRunTest {
22 | @Test
23 | fun benchmark() = runTest {
24 | repeat(1) {
25 | listOf(
26 | TilingBenchmark(
27 | pageToScrollTo = PAGE_TO_SCROLL_TO,
28 | pagesToInvalidate = emptyPages,
29 | ),
30 | TilingBenchmark(
31 | pageToScrollTo = PAGE_TO_SCROLL_TO,
32 | pagesToInvalidate = offScreenPages,
33 | ),
34 | TilingBenchmark(
35 | pageToScrollTo = PAGE_TO_SCROLL_TO,
36 | pagesToInvalidate = onScreenPages,
37 | ),
38 | PagingBenchmark(
39 | pageToScrollTo = PAGE_TO_SCROLL_TO,
40 | pagesToInvalidate = emptyPages,
41 | ),
42 | PagingBenchmark(
43 | pageToScrollTo = PAGE_TO_SCROLL_TO,
44 | pagesToInvalidate = offScreenPages,
45 | ),
46 | PagingBenchmark(
47 | pageToScrollTo = PAGE_TO_SCROLL_TO,
48 | pagesToInvalidate = onScreenPages,
49 | ),
50 | ).forEach { it.benchmark() }
51 | }
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/src/main/java/com/example/benchmarks/data/Benchmarked.kt:
--------------------------------------------------------------------------------
1 | package com.example.benchmarks.data
2 |
3 | const val PAGE_TO_SCROLL_TO = 20
4 | const val ITEMS_PER_PAGE = 100
5 |
6 | internal const val NUM_PAGES_IN_MEMORY = 3
7 | internal const val MAX_LOAD_SIZE = NUM_PAGES_IN_MEMORY * ITEMS_PER_PAGE
8 |
9 | @Suppress("EmptyRange")
10 | val emptyPages = 0 until 0
11 | val onScreenPages = (PAGE_TO_SCROLL_TO - NUM_PAGES_IN_MEMORY)..PAGE_TO_SCROLL_TO
12 | val offScreenPages = (onScreenPages.first - NUM_PAGES_IN_MEMORY) until onScreenPages.first
13 |
14 | sealed interface Benchmarked {
15 | suspend fun benchmark()
16 | }
17 |
18 | data class Item(
19 | val index: Int,
20 | val lastInvalidatedPage: Int = Int.MIN_VALUE
21 | )
22 |
23 | fun rangeFor(startPage: Int, numberOfPages: Int = 1): IntRange {
24 | val offset = startPage * ITEMS_PER_PAGE
25 | val next = offset + (ITEMS_PER_PAGE * numberOfPages)
26 |
27 | return offset until next
28 | }
29 |
30 | fun pageFor(item: Item): Int {
31 | val diff = item.index % ITEMS_PER_PAGE
32 | val firstItemIndex = item.index - diff
33 | return firstItemIndex / ITEMS_PER_PAGE
34 | }
35 |
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/src/main/java/com/example/benchmarks/data/Tiling.kt:
--------------------------------------------------------------------------------
1 | package com.example.benchmarks.data
2 |
3 | import com.tunjid.tiler.PivotRequest
4 | import com.tunjid.tiler.Tile
5 | import com.tunjid.tiler.TiledList
6 | import com.tunjid.tiler.listTiler
7 | import com.tunjid.tiler.queries
8 | import com.tunjid.tiler.toPivotedTileInputs
9 | import com.tunjid.tiler.toTiledList
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableSharedFlow
12 | import kotlinx.coroutines.flow.onStart
13 | import kotlinx.coroutines.flow.transform
14 | import kotlinx.coroutines.flow.transformWhile
15 |
16 | private val pivotRequest = PivotRequest(
17 | onCount = NUM_PAGES_IN_MEMORY,
18 | offCount = 0,
19 | comparator = Int::compareTo,
20 | nextQuery = Int::next,
21 | previousQuery = Int::prev,
22 | )
23 |
24 | private fun Int.next() = this + 1
25 |
26 | private fun Int.prev() = if (this <= 0) null else this - 1
27 |
28 | class TilingBenchmark(
29 | private val pageToScrollTo: Int,
30 | private val pagesToInvalidate: IntRange
31 | ) : Benchmarked {
32 |
33 | private var lastInvalidatedPage: Int = pagesToInvalidate.first + 1
34 |
35 | override suspend fun benchmark() {
36 | val offsetFlow = MutableSharedFlow()
37 | val invalidationSignal = MutableSharedFlow()
38 |
39 | offsetFlow
40 | .onStart { emit(0) }
41 | .toPivotedTileInputs(pivotRequest)
42 | .toTiledList(invalidationSignal.listTiler())
43 | .transformWhile { latestItems ->
44 | // Wait for items to finish loading
45 | if (latestItems.size != MAX_LOAD_SIZE) return@transformWhile true
46 |
47 | val firstPage = latestItems.queryAt(0)
48 | val lastPage = latestItems.queryAt(latestItems.lastIndex)
49 | val isOnLastPage = lastPage >= pageToScrollTo
50 |
51 | // Trigger loading more
52 | if (!isOnLastPage) {
53 | offsetFlow.emit(lastPage)
54 | return@transformWhile true
55 | }
56 |
57 | // Currently at the page scrolled to. If there's nothing to invalidate, complete
58 | if (pagesToInvalidate.isEmpty()) return@transformWhile false
59 |
60 | // ** Invalidation code ** //
61 |
62 | // Outside the visible range, nothing to invalidate so terminate
63 | if (pagesToInvalidate.first > lastPage) return@transformWhile false
64 | if (pagesToInvalidate.last < firstPage) return@transformWhile false
65 |
66 | println(latestItems.queries())
67 | // Find an item from the page that was invalidated
68 | val invalidatedItem = latestItems.lastInvalidatedItem()
69 |
70 | val isFinished = invalidatedItem != null
71 | && invalidatedItem.lastInvalidatedPage >= pagesToInvalidate.last
72 |
73 | emit(latestItems)
74 | !isFinished
75 | }
76 | .collect {
77 | // Invalidate
78 | val invalidatedItem = it.lastInvalidatedItem()
79 | val canIncrementAndInvalidate = invalidatedItem == null
80 | || invalidatedItem.lastInvalidatedPage == lastInvalidatedPage
81 |
82 | if (canIncrementAndInvalidate && ++lastInvalidatedPage <= pagesToInvalidate.last) {
83 | invalidationSignal.emit(lastInvalidatedPage)
84 | }
85 | }
86 | }
87 |
88 | private fun TiledList.lastInvalidatedItem(): Item? {
89 | for (item in this) {
90 | if (item.lastInvalidatedPage == lastInvalidatedPage) return item
91 | }
92 | return null
93 | }
94 | }
95 |
96 | private fun Flow.listTiler() = listTiler(
97 | limiter = Tile.Limiter(
98 | maxQueries = NUM_PAGES_IN_MEMORY,
99 | ),
100 | order = Tile.Order.PivotSorted(
101 | query = 0,
102 | comparator = Int::compareTo,
103 | ),
104 | fetcher = { page ->
105 | val range = rangeFor(page)
106 | transform { invalidatedPage ->
107 | if (invalidatedPage == page) emit(
108 | range.map {
109 | Item(
110 | index = it,
111 | lastInvalidatedPage = invalidatedPage
112 | )
113 | }
114 | )
115 | }
116 | .onStart {
117 | emit(range.map {
118 | Item(
119 | index = it,
120 | lastInvalidatedPage = Int.MIN_VALUE
121 | )
122 | })
123 | }
124 | },
125 | )
--------------------------------------------------------------------------------
/benchmarks/benchmarkable/src/test/java/com/example/benchmarks/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.benchmarks
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import com.example.benchmarks.data.PAGE_TO_SCROLL_TO
6 | import com.example.benchmarks.data.Item
7 | import com.example.benchmarks.data.ItemDao
8 | import com.example.benchmarks.data.PagingBenchmarks
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOf
11 | import kotlinx.coroutines.test.runTest
12 | import org.junit.Test
13 |
14 | import org.junit.Assert.*
15 |
16 | /**
17 | * Example local unit test, which will execute on the development machine (host).
18 | *
19 | * See [testing documentation](http://d.android.com/tools/testing).
20 | */
21 | class ExampleUnitTest {
22 | @Test
23 | fun useAppContext() = runTest {
24 | // TilingBenchmarks(Dao).scrollToIndex(ITEMS_TO_LOAD).run {
25 | // val at = indexOfFirst { it.id == ITEMS_TO_LOAD }
26 | // println("Tiling. id of $ITEMS_TO_LOAD at $at. Size: $size")
27 | // println(joinToString(separator = "\n"))
28 | // }
29 |
30 | PagingBenchmarks(Dao).scrollToIndex(PAGE_TO_SCROLL_TO).run {
31 | val at = indexOfFirst { it.id == PAGE_TO_SCROLL_TO }
32 | println("Tiling. id of $PAGE_TO_SCROLL_TO at $at. Size: $size")
33 | println(joinToString(separator = "\n"))
34 | }
35 | }
36 | }
37 |
38 | private object Dao : ItemDao {
39 | override fun itemPagingSource(): PagingSource = object : PagingSource() {
40 | override fun getRefreshKey(state: PagingState): Int? =
41 | state.anchorPosition
42 |
43 | override suspend fun load(
44 | params: LoadParams
45 | ): LoadResult = LoadResult.Page(
46 | data = params.key?.let { (it until (it + params.loadSize)).map(::Item) } ?: emptyList(),
47 | nextKey = params.key?.let { it + 1 },
48 | prevKey = params.key?.let {
49 | if (it <= 0) null
50 | else it - 1
51 | },
52 | )
53 | }
54 |
55 | override fun items(offset: Int, limit: Int): Flow> =
56 | flowOf((offset until (offset + limit)).map(::Item))
57 |
58 |
59 | override suspend fun upsertItems(items: List) {
60 | TODO("Not yet implemented")
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/benchmarks/microbenchmark/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/benchmarks/microbenchmark/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("kotlin-android")
4 | id("androidx.benchmark")
5 | id("android-library-convention")
6 | }
7 |
8 | kotlin {
9 | jvmToolchain(17)
10 | }
11 | android {
12 | namespace = "com.example.microbenchmark"
13 |
14 | defaultConfig {
15 | testInstrumentationRunnerArguments["androidx.benchmark.profiling.mode"] = "StackSampling"
16 | testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR,LOW-BATTERY"
17 | }
18 |
19 | testBuildType = "release"
20 |
21 | buildFeatures {
22 | compose = false
23 | }
24 | buildTypes {
25 | getByName("release") {
26 | // The androidx.benchmark plugin configures release buildType with proper settings, such as:
27 | // - disables code coverage
28 | // - adds CPU clock locking task
29 | // - signs release buildType with debug signing config
30 | // - copies benchmark results into build/outputs/connected_android_test_additional_output folder
31 | }
32 | }
33 | }
34 |
35 | dependencies {
36 | androidTestImplementation(project(":benchmarks:benchmarkable"))
37 | androidTestImplementation(libs.androidx.benchmark.junit)
38 | androidTestImplementation(libs.androidx.benchmark.macro)
39 | androidTestImplementation(libs.androidx.test.junit)
40 |
41 |
42 | androidTestImplementation(libs.kotlinx.coroutines.core)
43 | androidTestImplementation(libs.kotlinx.coroutines.test)
44 | }
45 |
--------------------------------------------------------------------------------
/benchmarks/microbenchmark/src/androidTest/java/com/example/microbenchmark/AllocationBenchmark.kt:
--------------------------------------------------------------------------------
1 | package com.example.microbenchmark
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import com.example.benchmarks.data.PAGE_TO_SCROLL_TO
7 | import com.example.benchmarks.data.PagingBenchmark
8 | import com.example.benchmarks.data.TilingBenchmark
9 | import com.example.benchmarks.data.emptyPages
10 | import com.example.benchmarks.data.offScreenPages
11 | import com.example.benchmarks.data.onScreenPages
12 | import kotlinx.coroutines.ExperimentalCoroutinesApi
13 | import kotlinx.coroutines.runBlocking
14 | import org.junit.FixMethodOrder
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.junit.runner.RunWith
18 | import org.junit.runners.MethodSorters
19 |
20 | /**
21 | * This is an example startup benchmark.
22 | *
23 | */
24 | @ExperimentalCoroutinesApi
25 | @RunWith(AndroidJUnit4::class)
26 | @FixMethodOrder(MethodSorters.NAME_ASCENDING)
27 | class AllocationBenchmark {
28 | @get:Rule
29 | val benchmarkRule = BenchmarkRule()
30 |
31 | @Test
32 | fun a_pagingScroll() = benchmarkRule.measureRepeated {
33 | runBlocking {
34 | PagingBenchmark(
35 | pageToScrollTo = PAGE_TO_SCROLL_TO,
36 | pagesToInvalidate = emptyPages
37 | ).benchmark()
38 | }
39 | }
40 |
41 | @Test
42 | fun b_tilingScroll() = benchmarkRule.measureRepeated {
43 | runBlocking {
44 | TilingBenchmark(
45 | pageToScrollTo = PAGE_TO_SCROLL_TO,
46 | pagesToInvalidate = emptyPages
47 | ).benchmark()
48 | }
49 | }
50 |
51 | @Test
52 | fun c_pagingInvalidationOffScreen() = benchmarkRule.measureRepeated {
53 | runBlocking {
54 | PagingBenchmark(
55 | pageToScrollTo = PAGE_TO_SCROLL_TO,
56 | pagesToInvalidate = offScreenPages
57 | ).benchmark()
58 | }
59 | }
60 |
61 | @Test
62 | fun d_tilingInvalidationOffScreen() = benchmarkRule.measureRepeated {
63 | runBlocking {
64 | TilingBenchmark(
65 | pageToScrollTo = PAGE_TO_SCROLL_TO,
66 | pagesToInvalidate = offScreenPages
67 | ).benchmark()
68 | }
69 | }
70 |
71 | @Test
72 | fun e_pagingInvalidationOnScreen() = benchmarkRule.measureRepeated {
73 | runBlocking {
74 | PagingBenchmark(
75 | pageToScrollTo = PAGE_TO_SCROLL_TO,
76 | pagesToInvalidate = onScreenPages
77 | ).benchmark()
78 | }
79 | }
80 |
81 | @Test
82 | fun f_tilingInvalidationOnScreen() = benchmarkRule.measureRepeated {
83 | runBlocking {
84 | TilingBenchmark(
85 | pageToScrollTo = PAGE_TO_SCROLL_TO,
86 | pagesToInvalidate = onScreenPages
87 | ).benchmark()
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/benchmarks/microbenchmark/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/build-logic/convention/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
18 |
19 |
20 | plugins {
21 | `kotlin-dsl`
22 | }
23 |
24 | group = "com.tunjid.tiler.buildlogic"
25 |
26 | java {
27 | sourceCompatibility = JavaVersion.VERSION_17
28 | targetCompatibility = JavaVersion.VERSION_17
29 | }
30 |
31 | kotlin {
32 | compilerOptions {
33 | jvmTarget.set(JvmTarget.JVM_17)
34 | }
35 | }
36 |
37 | dependencies {
38 | implementation(libs.jetbrains.compose.gradlePlugin)
39 | implementation(libs.kotlin.gradlePlugin)
40 | implementation(libs.android.gradlePlugin)
41 | implementation(libs.compose.compiler.plugin)
42 | implementation(libs.dokka.gradlePlugin)
43 | }
44 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/android-app-library-convention.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | import com.android.build.api.dsl.CommonExtension
18 | import org.gradle.api.JavaVersion
19 | import org.gradle.api.artifacts.Configuration
20 | import org.gradle.api.artifacts.VersionCatalogsExtension
21 |
22 | /*
23 | * Copyright 2021 Google LLC
24 | *
25 | * Licensed under the Apache License, Version 2.0 (the "License");
26 | * you may not use this file except in compliance with the License.
27 | * You may obtain a copy of the License at
28 | *
29 | * https://www.apache.org/licenses/LICENSE-2.0
30 | *
31 | * Unless required by applicable law or agreed to in writing, software
32 | * distributed under the License is distributed on an "AS IS" BASIS,
33 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34 | * See the License for the specific language governing permissions and
35 | * limitations under the License.
36 | */
37 |
38 | /**
39 | * Sets common values for Android Applications and Libraries
40 | */
41 | fun org.gradle.api.Project.androidConfiguration(
42 | extension: CommonExtension<*, *, *, *, *, *>
43 | ) = extension.apply {
44 | namespace = "com.tunjid.tiler.${project.name}"
45 | compileSdk = 36
46 |
47 | defaultConfig {
48 | minSdk = 23
49 | }
50 |
51 | compileOptions {
52 | sourceCompatibility = JavaVersion.VERSION_11
53 | targetCompatibility = JavaVersion.VERSION_11
54 | }
55 | configureKotlinJvm()
56 | }
57 |
58 | val org.gradle.api.Project.versionCatalog
59 | get() = extensions.getByType(VersionCatalogsExtension::class.java)
60 | .named("libs")
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/android-application-convention.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | plugins {
18 | id("com.android.application")
19 | }
20 |
21 | android {
22 | androidConfiguration(this)
23 |
24 | defaultConfig {
25 | targetSdk = 33
26 | }
27 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/android-library-convention.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | plugins {
18 | id("com.android.library")
19 | }
20 |
21 | android {
22 | androidConfiguration(this)
23 |
24 | sourceSets {
25 | named("main") {
26 | // Pull Android manifest from src/androidMain in multiplatform dirs
27 | if (file("src/androidMain").exists()) {
28 | manifest.srcFile("src/androidMain/AndroidManifest.xml")
29 | res.srcDirs("src/androidMain/res")
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/kotlin-jvm-convention.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | /*
18 | * Copyright 2021 Google LLC
19 | *
20 | * Licensed under the Apache License, Version 2.0 (the "License");
21 | * you may not use this file except in compliance with the License.
22 | * You may obtain a copy of the License at
23 | *
24 | * https://www.apache.org/licenses/LICENSE-2.0
25 | *
26 | * Unless required by applicable law or agreed to in writing, software
27 | * distributed under the License is distributed on an "AS IS" BASIS,
28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29 | * See the License for the specific language governing permissions and
30 | * limitations under the License.
31 | */
32 |
33 | plugins {
34 | kotlin("multiplatform")
35 | }
36 |
37 | kotlin {
38 | configureKotlinJvm()
39 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/kotlin-jvm-convention.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | import org.gradle.api.Action
18 | import org.gradle.api.JavaVersion
19 | import org.gradle.api.plugins.JavaPluginExtension
20 | import org.gradle.kotlin.dsl.configure
21 | import org.gradle.kotlin.dsl.withType
22 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
23 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
24 |
25 | /*
26 | * Copyright 2021 Google LLC
27 | *
28 | * Licensed under the Apache License, Version 2.0 (the "License");
29 | * you may not use this file except in compliance with the License.
30 | * You may obtain a copy of the License at
31 | *
32 | * https://www.apache.org/licenses/LICENSE-2.0
33 | *
34 | * Unless required by applicable law or agreed to in writing, software
35 | * distributed under the License is distributed on an "AS IS" BASIS,
36 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
37 | * See the License for the specific language governing permissions and
38 | * limitations under the License.
39 | */
40 |
41 | /**
42 | * Configure base Kotlin options for JVM (non-Android)
43 | */
44 | internal fun org.gradle.api.Project.configureKotlinJvm() {
45 | extensions.configure {
46 | // Up to Java 11 APIs are available through desugaring
47 | // https://developer.android.com/studio/write/java11-minimal-support-table
48 | sourceCompatibility = JavaVersion.VERSION_11
49 | targetCompatibility = JavaVersion.VERSION_11
50 | }
51 | configureKotlin()
52 | }
53 |
54 | /**
55 | * Configure base Kotlin options
56 | */
57 | private fun org.gradle.api.Project.configureKotlin() {
58 | // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947
59 | tasks.withType().configureEach {
60 | compilerOptions {
61 | // Set JVM target to 11
62 | jvmTarget.set(JvmTarget.JVM_11)
63 | freeCompilerArgs.set(
64 | freeCompilerArgs.get() + listOf(
65 | "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
66 | "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
67 | "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
68 | "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
69 | "-opt-in=kotlinx.coroutines.FlowPreview"
70 | )
71 | )
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/kotlin-library-convention.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
18 | import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
19 |
20 | /*
21 | * Copyright 2021 Google LLC
22 | *
23 | * Licensed under the Apache License, Version 2.0 (the "License");
24 | * you may not use this file except in compliance with the License.
25 | * You may obtain a copy of the License at
26 | *
27 | * https://www.apache.org/licenses/LICENSE-2.0
28 | *
29 | * Unless required by applicable law or agreed to in writing, software
30 | * distributed under the License is distributed on an "AS IS" BASIS,
31 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32 | * See the License for the specific language governing permissions and
33 | * limitations under the License.
34 | */
35 |
36 | plugins {
37 | kotlin("multiplatform")
38 | }
39 |
40 | kotlin {
41 | applyDefaultHierarchyTemplate()
42 | androidTarget {
43 | publishLibraryVariants("release")
44 | }
45 | jvm {
46 | testRuns["test"].executionTask.configure {
47 | useJUnit()
48 | }
49 | }
50 | listOf(
51 | iosX64(),
52 | iosArm64(),
53 | iosSimulatorArm64(),
54 | ).forEach { iosTarget ->
55 | iosTarget.binaries.framework {
56 | baseName = project.name
57 | isStatic = true
58 | }
59 | }
60 | sourceSets {
61 | all {
62 | languageSettings.apply {
63 | optIn("androidx.compose.animation.ExperimentalAnimationApi")
64 | optIn("androidx.compose.foundation.ExperimentalFoundationApi")
65 | optIn("androidx.compose.material.ExperimentalMaterialApi")
66 | optIn("androidx.compose.ui.ExperimentalComposeUiApi")
67 | optIn("kotlinx.serialization.ExperimentalSerializationApi")
68 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
69 | optIn("kotlinx.coroutines.FlowPreview")
70 | }
71 | }
72 | }
73 | configureKotlinJvm()
74 | }
75 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/publishing-library-convention.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | plugins {
18 | `maven-publish`
19 | signing
20 | id("org.jetbrains.dokka")
21 | }
22 |
23 | allprojects {
24 | val versionKey = project.name + "_version"
25 | val libProps = rootProject.ext.get("libProps") as? java.util.Properties
26 | ?: return@allprojects
27 | group = libProps["groupId"] as String
28 | version = libProps[versionKey] as String
29 |
30 | task("printProjectVersion") {
31 | doLast {
32 | println(">> " + project.name + " version is " + version)
33 | }
34 | }
35 | }
36 |
37 | val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) {
38 | dokkaSourceSets {
39 | try {
40 | named("iosTest") {
41 | suppress.set(true)
42 | }
43 | } catch (e: Exception) {
44 | }
45 | }
46 | }
47 |
48 | val javadocJar: TaskProvider by tasks.registering(Jar::class) {
49 | dependsOn(dokkaHtml)
50 | archiveClassifier.set("javadoc")
51 | from(dokkaHtml.outputDirectory)
52 | }
53 |
54 | publishing {
55 | publications {
56 | withType {
57 | artifact(javadocJar)
58 | pom {
59 | name.set(project.name)
60 | description.set("An experimental kotlin multiplatform paging library for loading reactive data in chunks")
61 | url.set("https://github.com/tunjid/tiler")
62 | licenses {
63 | license {
64 | name.set("Apache License 2.0")
65 | url.set("https://github.com/tunjid/tiler/blob/main/LICENSE")
66 | }
67 | }
68 | developers {
69 | developer {
70 | id.set("tunjid")
71 | name.set("Adetunji Dahunsi")
72 | email.set("tjdah100@gmail.com")
73 | }
74 | }
75 | scm {
76 | connection.set("scm:git:github.com/tunjid/tiler.git")
77 | developerConnection.set("scm:git:ssh://github.com/tunjid/tiler.git")
78 | url.set("https://github.com/tunjid/tiler/tree/main")
79 | }
80 | }
81 | }
82 | }
83 | repositories {
84 | val localProperties = rootProject.ext.get("localProps") as? java.util.Properties
85 | ?: return@repositories
86 |
87 | val publishUrl = localProperties.getProperty("publishUrl")
88 | if (publishUrl != null) {
89 | maven {
90 | name = localProperties.getProperty("repoName")
91 | url = uri(localProperties.getProperty("publishUrl"))
92 | credentials {
93 | username = localProperties.getProperty("username")
94 | password = localProperties.getProperty("password")
95 | }
96 | }
97 | }
98 | }
99 | }
100 |
101 |
102 | signing {
103 | val localProperties = rootProject.ext.get("localProps") as? java.util.Properties
104 | ?: return@signing
105 |
106 | val signingKey = localProperties.getProperty("signingKey")
107 | val signingPassword = localProperties.getProperty("signingPassword")
108 |
109 | if (signingKey != null && signingPassword != null) {
110 | useInMemoryPgpKeys(signingKey, signingPassword)
111 | sign(publishing.publications)
112 | }
113 | }
114 |
115 | val signingTasks = tasks.withType()
116 | tasks.withType().configureEach {
117 | dependsOn(signingTasks)
118 | }
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
18 |
19 | dependencyResolutionManagement {
20 | repositories {
21 | gradlePluginPortal()
22 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
23 | mavenCentral()
24 | google()
25 | }
26 | versionCatalogs {
27 | create("libs") {
28 | from(files("../gradle/libs.versions.toml"))
29 | }
30 | }
31 | }
32 |
33 | rootProject.name = "build-logic"
34 | include(":convention")
35 |
--------------------------------------------------------------------------------
/build-logic/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/build-logic/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/build-logic/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2021 Google LLC
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 | # https://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 | #Mon Jul 05 07:23:39 EDT 2021
18 | distributionBase=GRADLE_USER_HOME
19 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
20 | distributionPath=wrapper/dists
21 | zipStorePath=wrapper/dists
22 | zipStoreBase=GRADLE_USER_HOME
23 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | * https://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 | buildscript {
18 | extra.apply {
19 | set("localProps", java.util.Properties().apply {
20 | file("local.properties").let { file ->
21 | if (file.exists()) load(java.io.FileInputStream(file))
22 | }
23 | })
24 | set("libProps", java.util.Properties().apply {
25 | file("libraryVersion.properties").let { file ->
26 | if (file.exists()) load(java.io.FileInputStream(file))
27 | }
28 | })
29 | }
30 | repositories {
31 | google()
32 | mavenCentral()
33 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
34 | maven("https://plugins.gradle.org/m2/")
35 | }
36 | }
37 |
38 | plugins {
39 | alias(libs.plugins.android.application) apply false
40 | alias(libs.plugins.android.library) apply false
41 | alias(libs.plugins.androidx.benchmark) apply false
42 | alias(libs.plugins.compose.compiler) apply false
43 | alias(libs.plugins.jetbrains.compose) apply false
44 | alias(libs.plugins.jetbrains.dokka) apply false
45 | alias(libs.plugins.kotlin.android) apply false
46 | alias(libs.plugins.kotlin.multiplatform) apply false
47 | }
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
--------------------------------------------------------------------------------
/docs/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/assets/logo.png
--------------------------------------------------------------------------------
/docs/images/adaptive.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/images/adaptive.gif
--------------------------------------------------------------------------------
/docs/images/basic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/images/basic.gif
--------------------------------------------------------------------------------
/docs/images/complex.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/images/complex.gif
--------------------------------------------------------------------------------
/docs/images/placeholders.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/images/placeholders.gif
--------------------------------------------------------------------------------
/docs/images/search.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/images/search.gif
--------------------------------------------------------------------------------
/docs/images/sticky.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tunjid/Tiler/59ed8d279713f8535b032ca6601eef29f8e3768c/docs/images/sticky.gif
--------------------------------------------------------------------------------
/docs/implementation/performance.md:
--------------------------------------------------------------------------------
1 | # Efficiency & performance
2 |
3 | ## General Tiling
4 |
5 | As tiling loads from multiple flows simultaneously, performance is a function of 2 things:
6 |
7 | * How often the backing `Flow` for each `Input.Request` emits
8 | * The time and space complexity of the transformations applied to the
9 | output `TiledList`.
10 |
11 | In the case of a former, the `Flow` should only emit if the backing dataset has actually changed.
12 | This prevents unnecessary emissions downstream.
13 |
14 | In the case of the latter, use `PivotRequest(on = x)` and `Input.Limiter` to match the
15 | output `TiledList` to the size of the view port of the user's device to create an efficient
16 | paging pipeline.
17 |
18 | For example if tiling is done for the UI, with a viewport that can display 20 items at once:
19 |
20 | * 20 items can be fetched per page
21 | * 100 items (20 * 5 pages) can be observed at concurrently
22 | * `Input.Limiter.List(maxQueries = 3)` can be set so only changes to the visible 60 items will be
23 | sent to the UI at once.
24 |
25 | The items can be transformed with algorithms of `O(N)` to `O(N^2)` time and space complexity
26 | trivially as regardless of the size of the actual paginated set, only 60 items will be transformed
27 | at any one time.
28 |
29 | ## MutableTiledList
30 |
31 | The performance of each method for the default `MutableTiledList` implementation is
32 | comparable to the cost of the same method in an `ArrayList` + O(log(T))
33 | where T is the number of `Tile` instances (queries/pages) in the `TiledList`.
34 | This makes them perfect for use in recycling and scrolling containers.
35 |
--------------------------------------------------------------------------------
/docs/implementation/pivoted-tiling.md:
--------------------------------------------------------------------------------
1 | ## How to page with Tiling
2 |
3 | While the tiling API lets you assemble a paging pipeline from scratch using its primitives, the
4 | easiest scalable
5 | pagination approach with tiling is through the pivoting algorithm.
6 |
7 | Consider a large, possibly infinite set of paginated data where a user is currently viewing page p,
8 | and n
9 | is the buffer zone - the number of pages lazy loaded in case the user wants to visit it.
10 |
11 | ```
12 | [..., p - n, ..., p - 1, p, p + 1, ..., p + n, ...]
13 | ```
14 |
15 | As the user moves from page to page, items can be refreshed around the user's current page
16 | while allowing them to observe immediate changes to the data they're looking at.
17 |
18 | This is expanded in the diagram below:
19 |
20 | ```
21 | [out of bounds] -> Evict from memory
22 | _
23 | [p - n - 1 - n] |
24 | ... | -> Keep pages in memory, but don't observe
25 | [p - n - 1] _ _|
26 | [p - n] |
27 | ... |
28 | [p - 1] |
29 | [p] | -> Observe pages
30 | [p + 1] |
31 | ... |
32 | [p + n] _| _
33 | [p + n + 1] |
34 | ... | -> Keep pages in memory, but don't observe
35 | [p + n + 1 + n] _|
36 |
37 | [out of bounds] -> Evict from memory
38 | ```
39 |
40 | `n` is an arbitrary number that may be defined by how many items are visible on the screen at once.
41 | It could be fixed,
42 | or variable depending on conditions like the available screen real estate.
43 |
44 | For an example where `n` is a function of grid size in a grid list, check
45 | out [ArchiveLoading.kt](https://github.com/tunjid/me/blob/main/common/feature-archive-list/src/commonMain/kotlin/com/tunjid/me/feature/archivelist/ArchiveLoading.kt)
46 | in the [me](https://github.com/tunjid/me) project.
47 |
48 | The above algorithm is called "pivoting" as items displayed are pivoted around the user's current
49 | scrolling position.
50 |
51 | Since tiling is dynamic at it's core, a pipeline can be built to allow for this dynamic behavior by
52 | pivoting around the user's current position with the grid size as a dynamic input parameter.
--------------------------------------------------------------------------------
/docs/implementation/primitives.md:
--------------------------------------------------------------------------------
1 | ## API surface and Tiling primitives
2 |
3 | Given a `Flow` of `Tile.Input`, tiling transforms them into a `Flow>` with
4 | a `ListTiler`.
5 |
6 | The resulting `TiledList` should be kept at a size that covers approximately 3 times the viewport.
7 | This is typically at or under 100 items for non grid UIs. You can then transform this list however
8 | way you want.
9 |
10 | ## Managing requested data
11 |
12 | A tiled pagination pipeline is managed by the `Tile.Input` it receives. These inputs drive the
13 | dynamism of the pipeline. The following is a breakdown of them all.
14 |
15 | ### `Input.Request`
16 |
17 | * `Tile.Request.On`: Analogous to `put` for a `Map`, this starts collecting from the backing `Flow`
18 | for the specified `query`. It is
19 | idempotent; multiple requests have no side effects for loading, i.e the same `Flow` will not be
20 | collect twice.
21 |
22 | * `Tile.Request.Off`: Stops collecting from the backing `Flow` for the specified `query`. The items
23 | previously fetched by this query
24 | are still kept in memory and will be in the `TiledList` of items returned. Requesting this is
25 | idempotent; multiple requests
26 | have no side effects.
27 |
28 | * `Tile.Request.Evict`: Analogous to `remove` for a `Map`, this stops collecting from the
29 | backing `Flow` for the specified `query` and
30 | also evicts the items previously fetched by the `query` from memory. Requesting this is
31 | idempotent; multiple requests
32 | have no side effects.
33 |
34 | ### `Tile.Batch`
35 |
36 | Used for dispatching multiple `Tile.Input` instances. The `ListTiler` may emit `TiledList` instances
37 | during the application of a `Tile.Batch` input; it is not transactional. Rather, it is an
38 | encapsulation of an aggregate of `Tile.Input` that represents a single logical operation. Users of
39 | the library may also define arbitrary `Tile.Batch` instances and use them in their tiled pipelines.
40 |
41 | ### `Pivot`
42 |
43 | An implementation of `Tile.Batch`, this allows for returning a `TiledList` from results
44 | around a particular `Query`. It's use must be accompanied by a `Tile.Order.PivotSorted`.
45 |
46 | ### `Tile.Input.Limiter`
47 |
48 | Can be used to select a subset of items tiled instead of the entire paginated set. For example,
49 | assuming 1000 items have been
50 | fetched, there's no need to send a 1000 items to the UI for diffing/display when the UI can only
51 | show about 30 at once.
52 | The `Limiter` allows for selecting an arbitrary amount of items as the situation demands.
53 |
54 | ### `Tile.Input.Order`
55 |
56 | Defines the heuristic for selecting tiled items into the output `TiledList`.
57 |
58 | * Sorted: Sort items with a specified query `comparator`.
59 |
60 | * PivotSorted: Sort items with the specified `comparator` but pivoted around a specific `Query`.
61 | This allows for showing items that have more priority over others in the current context
62 | like example in a list being scrolled. In other words assume tiles have been fetched for queries
63 | 1 - 10 but a
64 | user can see pages 5 and 6. The UI need only to be aware of pages 4, 5, 6, and 7. This allows for
65 | a rolling window of
66 | queries based on a user's scroll position.
67 |
68 |
--------------------------------------------------------------------------------
/docs/implementation/tiledlist.md:
--------------------------------------------------------------------------------
1 | A `TiledList` is a `List` that:
2 |
3 | * Is a sublist of the items in the backing data source.
4 | * Allows for looking up the query that fetched each item.
5 |
6 | The latter is done by associating a range of indices in the `List` with a `Tile`.
7 | Effectively, a `TiledList` "chunks" its items by query.
8 | For example, the `TiledList` below is a `List` with 10 items, and two tiles. Each `Tile` covers 5
9 | indices:
10 |
11 | ```
12 | | 1 | 2 |
13 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
14 | ```
15 |
16 | A `Tile` is a `value` class with the following public properties:
17 |
18 | ```kotlin
19 | value class Tile(...) {
20 | // start index for a chunk
21 | val start: Int
22 |
23 | // end exclusive index for a chunk
24 | val end: Int
25 | }
26 | ```
27 |
28 | A `TiledList` is defined as:
29 |
30 | ```kotlin
31 | interface TiledList : List {
32 | /**
33 | * The number of [Tile] instances or query ranges there are in this [TiledList]
34 | */
35 | val tileCount: Int
36 |
37 | /**
38 | * Returns the [Tile] at the specified tile index.
39 | */
40 | fun tileAt(tileIndex: Int): Tile
41 |
42 | /**
43 | * Returns the query at the specified tile index.
44 | */
45 | fun queryAtTile(tileIndex: Int): Query
46 |
47 | /**
48 | * Returns the query that fetched an [Item] at a specified index.
49 | */
50 | fun queryAt(index: Int): Query
51 | }
52 | ```
53 |
54 | `MutableTiledList` instances also exist:
55 |
56 | ```kotlin
57 | interface MutableTiledList : TiledList {
58 | fun add(index: Int, query: Query, item: Item)
59 |
60 | fun add(query: Query, item: Item): Boolean
61 |
62 | fun addAll(query: Query, items: Collection): Boolean
63 |
64 | fun addAll(index: Int, query: Query, items: Collection): Boolean
65 |
66 | fun remove(index: Int): Item
67 | }
68 | ```
69 |
70 | This is useful for modifying `TiledList` instances returned. Actions like:
71 |
72 | * Inserting separators or other interstitial content
73 | * Mapping items with in memory data after fetching from a database
74 | * General list modification
75 |
76 | can be easily performed.
--------------------------------------------------------------------------------
/docs/usecases/adaptive-paging.md:
--------------------------------------------------------------------------------
1 | The following guide helps create the UI/UX seen below:
2 |
3 |
4 |
5 |
6 |
7 | ## Guide
8 |
9 | Situations can arise that can require more dynamic pagination.
10 | Consider paging for an adaptive layout. The items fetched can be a function of:
11 |
12 | * A UI that can change in size, requiring more items to fit the view port.
13 | * A user wanting to change the sort order.
14 |
15 | A pivoting pipeline for the above looks like:
16 |
17 | ```kotlin
18 |
19 | // Query for items describing the page and sort order
20 | data class PageQuery(
21 | val page: Int,
22 | val isAscending: Boolean
23 | )
24 |
25 | class Loader(
26 | isDark: Boolean,
27 | scope: CoroutineScope
28 | ) {
29 | // Current query that is visible in the view port
30 | private val currentQuery = MutableStateFlow(
31 | PageQuery(
32 | page = 0,
33 | isAscending = true
34 | )
35 | )
36 |
37 | // Number of columns in the grid
38 | private val numberOfColumns = MutableStateFlow(1)
39 |
40 | // Flow specifying the pivot configuration
41 | private val pivotRequests = combine(
42 | currentQuery.map { it.isAscending },
43 | numberOfColumns,
44 | ::pivotRequest
45 | ).distinctUntilChanged()
46 |
47 | // Define inputs that match the current pivoted position
48 | private val pivotInputs = currentQuery.toPivotedTileInputs(
49 | pivotRequests = pivotRequests
50 | )
51 |
52 | // Allows for changing the order on response to user input
53 | private val orderInputs = currentQuery
54 | .map { pageQuery ->
55 | Tile.Order.PivotSorted(
56 | query = pageQuery,
57 | comparator = when {
58 | pageQuery.isAscending -> ascendingPageComparator
59 | else -> descendingPageComparator
60 | }
61 | )
62 | }
63 | .distinctUntilChanged()
64 |
65 | // Change limit to account for dynamic view port size
66 | private val limitInputs = numberOfColumns.map { gridSize ->
67 | Tile.Limiter { items -> items.size > MIN_ITEMS_TO_SHOW * gridSize }
68 | }
69 |
70 | val tiledList: Flow> = merge(
71 | pivotInputs,
72 | orderInputs,
73 | limitInputs,
74 | )
75 | .toTiledList(
76 | numberTiler(
77 | itemsPerPage = ITEMS_PER_PAGE,
78 | isDark = isDark,
79 | )
80 | )
81 |
82 | fun setCurrentPage(page: Int) = currentQuery.update { query ->
83 | query.copy(page = page)
84 | }
85 |
86 | fun toggleOrder() = currentQuery.update { query ->
87 | query.copy(isAscending = !query.isAscending)
88 | }
89 |
90 | fun setNumberOfColumns(numberOfColumns: Int) = this.numberOfColumns.update {
91 | numberOfColumns
92 | }
93 |
94 | // Avoid breaking object equality in [PivotRequest] by using vals
95 | private val nextQuery: PageQuery.() -> PageQuery? = {
96 | copy(page = page + 1)
97 | }
98 | private val previousQuery: PageQuery.() -> PageQuery? = {
99 | copy(page = page - 1).takeIf { it.page >= 0 }
100 | }
101 |
102 | /**
103 | * Pivoted tiling with the grid size as a dynamic input parameter
104 | */
105 | private fun pivotRequest(
106 | isAscending: Boolean,
107 | numberOfColumns: Int,
108 | ) = PivotRequest(
109 | onCount = 4 * numberOfColumns,
110 | offCount = 4 * numberOfColumns,
111 | nextQuery = nextQuery,
112 | previousQuery = previousQuery,
113 | comparator = when {
114 | isAscending -> ascendingPageComparator
115 | else -> descendingPageComparator
116 | }
117 | )
118 | }
119 |
120 | private fun numberTiler(
121 | itemsPerPage: Int,
122 | isDark: Boolean,
123 | ): ListTiler =
124 | listTiler(
125 | limiter = Tile.Limiter { items -> items.size > 40 },
126 | order = Tile.Order.PivotSorted(
127 | query = PageQuery(page = 0, isAscending = true),
128 | comparator = ascendingPageComparator
129 | ),
130 | fetcher = { pageQuery ->
131 | pageQuery.colorShiftingTiles(itemsPerPage, isDark)
132 | }
133 | )
134 |
135 | fun PageQuery.colorShiftingTiles(itemsPerPage: Int, isDark: Boolean): Flow> {
136 | ...
137 | }
138 | ```
139 |
140 | In the above, only flows for 4 * numOfColumns queries are collected at any one time. 4 * numOfColumns more queries are kept in memory for quick
141 | resumption, and the rest are evicted from memory. As the user scrolls, `setCurrentPage` is called, and data is
142 | fetched for that page, and the surrounding pages.
143 | Pages that are far away from the defined range are removed from memory.
--------------------------------------------------------------------------------
/docs/usecases/basic-example.md:
--------------------------------------------------------------------------------
1 | # Basic example
2 |
3 | The following guide should help create the UI/UX seen below:
4 |
5 |
6 |
7 |
8 |
9 | ## Guide
10 |
11 | Imagine a social media feed app backed by a `FeedRepository`.
12 | Each page in the repository returns 30 items. A pivoted tiling pipeline for it can be assembled as
13 | follows:
14 |
15 | ```kotlin
16 | class FeedState(
17 | repository: FeedRepository
18 | ) {
19 | private val requests = MutableStateFlow(0)
20 |
21 | private val comparator = Comparator(Int::compareTo)
22 |
23 | // A TiledList is a regular List that has information about what
24 | // query fetched an item at each index
25 | val feed: StateFlow> = requests
26 | .toPivotedTileInputs(
27 | PivotRequest(
28 | // 5 pages are fetched concurrently, so 150 items
29 | onCount = 5,
30 | // A buffer of 2 extra pages on either side are kept, so 210 items total
31 | offCount = 2,
32 | comparator = comparator,
33 | nextQuery = {
34 | this + 1
35 | },
36 | previousQuery = {
37 | (this - 1).takeIf { it >= 0 }
38 | }
39 | )
40 | )
41 | .toTiledList(
42 | listTiler(
43 | // Start by pivoting around 0
44 | order = Tile.Order.PivotSorted(
45 | query = 0,
46 | comparator = comparator
47 | ),
48 | // Limit to only 3 pages of data in UI at any one time, so 90 items
49 | limiter = Tile.Limiter(
50 | maxQueries = 3,
51 | itemSizeHint = null,
52 | ),
53 | fetcher = { page ->
54 | // The fetcher returns a flow, this allows for self updating pages
55 | flow { emit(repository.getPage(page)) }
56 | }
57 | )
58 | )
59 | .stateIn(/*...*/)
60 |
61 | fun setVisiblePage(page: Int) {
62 | requests.value = page
63 | }
64 | }
65 | ```
66 |
--------------------------------------------------------------------------------
/docs/usecases/complex-tiling.md:
--------------------------------------------------------------------------------
1 | The following guide should help create the UI/UX seen below:
2 |
3 |
4 |
5 |
6 |
7 | See the `ArchiveList` state production pipeline in
8 | the [me](https://github.com/tunjid/me/blob/main/common/ui/archive-list/src/commonMain/kotlin/com/tunjid/me/feature/archivelist/ArchiveListStateHolder.kt)
9 | github project for an example of a a complex tiled pagination pipeline with key preservation across
10 | multiple queries. In it, the current item the user is viewing can remain anchored even as the
11 | search filter changes.
12 |
13 | ## Guide
14 |
15 | In your application you may have scenarios that are a combination of all the use cases covered:
16 |
17 | * Adapts to different screen sizes
18 | * Uses placeholders
19 | * Implements search
20 | * Requires extra transformations
21 |
22 | In situations like this, the preceding sections still apply. Each use case is
23 | independent by and large. That said, the most difficult issue faced with combined use cases is
24 | key preservation.
25 |
26 | # Key preservation
27 |
28 | In UIs, keys provide unique tokens to represent individual items in lists. This is necessary for
29 | scroll state preservation and animations. In a tiled paging pipeline where items change due to:
30 |
31 | * Placeholders being replaced
32 | * Items being sorted or reordered differently
33 | * Generic items being replaced
34 | * Search queries changing
35 |
36 | The best way to preserve keys across these changes is to maintain a snapshot of the old list,
37 | and when the new list arrives, preserve the keys in the old list in the new list by defining
38 | a way to identify them in the new list.
39 |
40 | ### On each query...
41 |
42 | In a range of items in a query/page that has 20 items:
43 |
44 | * First generate 20 unique ids for all items in that range.
45 | * Create placeholders that use those ids and emit them.
46 | * Asynchronously load and emit new items, and use the same ids for the placeholders in those items.
47 |
48 | ### On each `TiledList` emission...
49 |
50 | * Keep a reference to the current list presented in the UI
51 | * When the new `TiledList` is emitted, compare the old list to the new list
52 | * If the old list has items that are not placeholders that are present in the new list, replace the
53 | ids in the new list with the ids from the old list.
54 | * Make sure ids are not duplicated.
55 |
56 | The steps above will allow you to achieve smooth item animations in complex pagination pipelines.
57 |
--------------------------------------------------------------------------------
/docs/usecases/compose.md:
--------------------------------------------------------------------------------
1 | ## Pivoting with Jetpack Compose
2 |
3 | Pivoted tiling in Jetpack Compose is done using the `PivotedTilingEffect`:
4 |
5 | ```kotlin
6 | @Composable
7 | fun Feed(
8 | state: FeedState
9 | ) {
10 | val feed by state.feed.collectAsState()
11 | val lazyState = rememberLazyListState()
12 |
13 | LazyColumn(
14 | state = lazyState,
15 | content = {
16 | items(
17 | items = feed,
18 | key = FeedItem::key,
19 | itemContent = { /*...*/ }
20 | )
21 | }
22 | )
23 |
24 | lazyState.PivotedTilingEffect(
25 | items = feed,
26 | // Update the user's current visible query
27 | onQueryChanged = { page -> if (it != null) state.setVisiblePage(page) }
28 | )
29 | }
30 | ```
31 |
32 | As the user scrolls, `setVisiblePage` is called to keep pivoting about the current position.
33 |
34 | ## Unique keys
35 |
36 | Tiling collects from each `Flow` for all queries that are on concurrently. For pagination from
37 | a database where items can be inserted, items may be duplicated in the produced `TIledList`.
38 |
39 | For example consider a DB table consisting of tasks sorted by ascending date:
40 |
41 | | id | date | task |
42 | |------|----------|-----------------------|
43 | | ... | | |
44 | | 0998 | 01/01/23 | Go for a jog |
45 | | 0999 | 01/04/23 | Print shipping labels |
46 | | ... | | |
47 |
48 | Assuming 20 items per query, tasks 0980 - 0999 will be contained in a query for page 50.
49 |
50 | Assume a new task for "Check invoices" with id 1000 is entered for date 01/03/23:
51 |
52 | | id | date | task |
53 | |------|----------|-----------------------|
54 | | ... | | |
55 | | 0998 | 01/01/23 | Go for a jog |
56 | | 1000 | 01/03/23 | Check invoices |
57 | | 0999 | 01/04/23 | Print shipping labels |
58 | | ... | | |
59 |
60 | There are now 51 pages of tasks. When page 51 emits:
61 |
62 | * It will contain the last task alone; task 099 "Print shipping labels".
63 | * Page 50 will still have its last emitted tasks 0980 - 0999, including "Print shipping labels".
64 | * At some point in the future, page 50 will update to contain the new task 1000 "Check invoices" and
65 | exclude task 0999 - "Print shipping labels".
66 |
67 | Until page 50 updates, task 0999 "Print shipping labels" will be duplicated in the list. To address
68 | this, the
69 | produced `TiledList` will need to be filtered for duplicates since keys must be unique in Compose
70 | lazy layouts and indices cannot be used for keys without losing animations.
71 |
72 | This is easily done using `TiledList.distinct()` or `TiledList.distinctBy()`. The cost of this fixed
73 | since a `TiledList` is a sublist of the entire collection. Using a pivoted tiling
74 | pipeline where 5 queries are kept on, but 3 queries are presented to the UI at any one
75 | time (using `Tile.Limiter`), the fixed cost for de-duplicating items for every change in the
76 | data set is O(60).
77 |
78 | Note: Page 51 is not guaranteed to have emitted first. Any query can emit at anytime
79 | when tiling. Tiling presents snapshots of the paging pipeline at a single point in time. It is not
80 | opinionated about the data contained. It only guarantees ordering of the queries according to the
81 | `Tile.Order` specified in the tiling configuration. This makes it flexible enough for post
82 | processing of data like filtering, debouncing, mapping and so on.
83 |
84 | ## Sticky headers
85 |
86 | For a `LazyList` in Compose, sticky headers can be implemented using the following:
87 |
88 | ```kotlin
89 | // This ideally would be done in the ViewModel
90 | val grouped = contacts.groupBy { it.firstName[0] }
91 |
92 | @OptIn(ExperimentalFoundationApi::class)
93 | @Composable
94 | fun ContactsList(grouped: Map>) {
95 | LazyColumn {
96 | grouped.forEach { (initial, contactsForInitial) ->
97 | stickyHeader {
98 | CharacterHeader(initial)
99 | }
100 |
101 | items(contactsForInitial) { contact ->
102 | ContactListItem(contact)
103 | }
104 | }
105 | }
106 | }
107 | ```
108 |
109 | When paging with a `TiledList`, grouping can still be performed. If you do not need `TiledList`
110 | metadata on the grouped data use `List.groupBy()`, otherwise use `TiledList.groupBy()` which will
111 | return a `Map`.
112 |
--------------------------------------------------------------------------------
/docs/usecases/overview.md:
--------------------------------------------------------------------------------
1 | As tiling is a pure function that operates on a reactive stream, its configuration can be changed on
2 | the fly.
3 | This lends it well to the following situations:
4 |
5 | * Offline-first apps: Tiling delivers targeted updates to only queries that have changed. This works
6 | well for
7 | apps which write to the database as the source of truth, and need the UI to update immediately.
8 | For example
9 | a viral tweet whose like count updates several times a second.
10 |
11 | * Adaptive pagination: The amount of items paginated through can be adjusted dynamically to account
12 | for app window
13 | resizing by turning [on](https://github.com/tunjid/Tiler#inputrequest) more pages and increasing
14 | the
15 | [limit](https://github.com/tunjid/Tiler#inputlimiter) of data sent to the UI from the paginated
16 | data available.
17 | An example is in
18 | the [Me](https://github.com/tunjid/me/blob/main/common/feature-archive-list/src/commonMain/kotlin/com/tunjid/me/feature/archivelist/ArchiveLoading.kt)
19 | app.
20 |
21 | * Dynamic sort order: The sort order of paginated items can be changed cheaply on a whim by changing
22 | the
23 | [order](https://github.com/tunjid/Tiler#inputorder) as this only operates on the data output from
24 | the tiler, and not
25 | the entire paginated data set. An example is in the sample in this
26 | [repository](https://github.com/tunjid/Tiler/blob/develop/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/numbers/advanced/NumberFetching.kt).
27 |
--------------------------------------------------------------------------------
/docs/usecases/placeholders.md:
--------------------------------------------------------------------------------
1 | # Placeholders
2 |
3 | The following guide helps create the UI/UX seen below:
4 |
5 |
6 |
7 |
8 |
9 | The code for the above can be seen in the Musify Spotify clone, on the Podcast
10 | episode
11 | detail [screen](https://github.com/tunjid/Musify/blob/main/app/src/main/java/com/example/musify/ui/screens/searchscreen/StateProduction.kt).
12 |
13 | ## Guide
14 |
15 | When loading data from asynchronous sources, it is sometimes required to show static data first.
16 | Since tiling exposes a `List`, inserting placeholders typically involves emitting the placeholder
17 | items first.
18 |
19 | Consider the following repository that fetches a list of podcast episodes from the network with
20 | paginated offsets:
21 |
22 | ```kotlin
23 |
24 | private const val LIMIT = 20
25 |
26 | /**
27 | * A query for tracks at a certain offset
28 | */
29 | data class PodcastEpisodeQuery(
30 | val offset: Int,
31 | val limit: Int = LIMIT,
32 | )
33 |
34 | interface PodcastEpisodeRepository {
35 | suspend fun episodesFor(query: PodcastEpisodeQuery): List