Milan Jurkulak 3 роки тому
батько
коміт
4c2d317732

+ 2 - 0
app/build.gradle.kts

@@ -136,6 +136,8 @@ dependencies {
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-SNAPSHOT")
     implementation("androidx.tv:tv-material:1.0.0-alpha07")
+    // paging
+    implementation("androidx.paging:paging-compose:3.2.0-rc01")
     // view model
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
     // navigation

+ 70 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/paging/ListPagingSource.kt

@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.paging
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import kotlinx.coroutines.delay
+
+typealias SOURCE<T> = suspend (page: Int, cnt: Int) -> List<T>
+
+class ListPagingSource<T : Any>(
+    private val source: SOURCE<T> = { _, _ -> emptyList() },
+    private val maxRetryCount: Int = DEFAULT_MAX_RETRY_COUNT,
+    private val retryDelay: Long = DEFAULT_RETRY_DELAY,
+) : PagingSource<Int, T>() {
+
+    companion object {
+        const val DEFAULT_MAX_RETRY_COUNT = 10
+        const val DEFAULT_RETRY_DELAY = 250L // 250 ms
+    }
+
+    override fun getRefreshKey(state: PagingState<Int, T>): Int? {
+        return state.anchorPosition?.let { anchorPosition ->
+            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+        }
+    }
+
+    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
+        return try {
+            val page = params.key ?: 1
+            val response: List<T> = retrySuspend(
+                retryDelay = retryDelay,
+                maxRetryCount = maxRetryCount,
+                condition = { size >= params.loadSize },
+                block = { source.invoke(page, params.loadSize) }
+            )
+            return LoadResult.Page(
+                data = response,
+                prevKey = if (page == 1) null else page.minus(1),
+                nextKey = if (response.isEmpty()) null else page.plus(1),
+            )
+        } catch (e: Exception) {
+            LoadResult.Error(e)
+        }
+    }
+
+    private suspend fun <T> retrySuspend(
+        retryDelay: Long = DEFAULT_RETRY_DELAY,
+        maxRetryCount: Int = DEFAULT_MAX_RETRY_COUNT,
+        condition: T.() -> Boolean,
+        block: suspend () -> T
+    ): T {
+        var retryCount = maxRetryCount
+        var ret = block.invoke()
+        while ((!condition.invoke(ret)) && (retryCount > 0)) {
+            delay(retryDelay)
+            ret = block.invoke()
+            retryCount -= 1
+        }
+        return ret
+    }
+
+}

+ 91 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/paging/PagingTvLazyRow.kt

@@ -0,0 +1,91 @@
+package org.mjdev.tvapp.base.paging
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.Alignment
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.paging.LoadState
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.paging.compose.itemContentType
+import androidx.paging.compose.itemKey
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.list.TvLazyListState
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.text.TextAny
+
+@Suppress("ModifierParameter")
+@TvPreview
+@Composable
+fun <T : Any> PagingTvLazyRow(
+    modifier: Modifier = Modifier.fillMaxSize(),
+    state: TvLazyListState = rememberTvLazyListState(),
+    contentPadding: Dp = 0.dp,
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start
+    else Arrangement.End,
+    verticalAlignment: Alignment.Vertical = Alignment.Top,
+    userScrollEnabled: Boolean = true,
+    pivotOffsets: PivotOffsets = PivotOffsets(),
+    perPage: Int = 8,
+    source: SOURCE<T> = { _, _ ->
+        emptyList()
+    },
+    loadStateHandler: (state: LoadState) -> Unit = {},
+    onItemClick: (data: T) -> Unit = { _ -> },
+    emptyContent: @Composable () -> Unit = {
+        TextAny(text = R.string.no_items)
+    },
+    itemBlock: @Composable (
+        idx: Int, item: T, onItemClick: (data: T) -> Unit
+    ) -> Unit = { _, _, _ -> }
+) {
+
+    val listData = remember {
+        Pager(config = PagingConfig(
+            pageSize = perPage, prefetchDistance = perPage, initialLoadSize = perPage
+        ), pagingSourceFactory = {
+            ListPagingSource(source)
+        }).flow
+    }.collectAsLazyPagingItems().apply {
+        loadStateHandler.invoke(loadState.refresh)
+    }
+
+    TvLazyRow(
+        modifier = modifier,
+        state = state,
+        contentPadding = PaddingValues(contentPadding),
+        reverseLayout = reverseLayout,
+        horizontalArrangement = horizontalArrangement,
+        verticalAlignment = verticalAlignment,
+        userScrollEnabled = userScrollEnabled,
+        pivotOffsets = pivotOffsets,
+    ) {
+        if (listData.itemCount == 0) {
+            item {
+                emptyContent()
+            }
+        } else {
+            items(
+                count = listData.itemCount,
+                key = listData.itemKey(),
+                contentType = listData.itemContentType()
+            ) { index ->
+                val item = listData[index]
+                if (item != null) {
+                    itemBlock(index, item, onItemClick)
+                }
+            }
+        }
+    }
+
+}

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/TVRow.kt

@@ -23,7 +23,7 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvapp.base.annotations.TvPreview
 
-// scroll horizontally
+// todo : scroll horizontally by touch
 @TvPreview
 @Composable
 fun TVRow(

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -34,5 +34,6 @@
 
     <string name="author">Created by Milan Jurkulak © 2023</string>
     <string name="title_plugins">Plugins</string>
+    <string name="no_items">no items</string>
 
 </resources>