Skip to content

Commit 1fd8a2b

Browse files
committed
✨ [useTableRequest]: Add paginated table request hook
- Add useTableRequest hook for async table data fetching - Integrate with useRequest for caching and request management - Add example with enhanced pagination UI - Remove deprecated AsyncTable types and tests
1 parent 14804b5 commit 1fd8a2b

3 files changed

Lines changed: 560 additions & 0 deletions

File tree

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package xyz.junerver.composehooks.example
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.*
5+
import androidx.compose.foundation.lazy.LazyColumn
6+
import androidx.compose.material.icons.Icons
7+
import androidx.compose.material.icons.filled.KeyboardArrowDown
8+
import androidx.compose.material.icons.filled.KeyboardArrowUp
9+
import androidx.compose.material.icons.filled.Refresh
10+
import androidx.compose.material3.*
11+
import androidx.compose.runtime.*
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.text.font.FontWeight
15+
import androidx.compose.ui.unit.dp
16+
import kotlinx.coroutines.delay
17+
import xyz.junerver.compose.hooks.userequest.TableResult
18+
import xyz.junerver.compose.hooks.userequest.useTableRequest
19+
import xyz.junerver.compose.hooks.usetable.core.column
20+
import xyz.junerver.compose.hooks.usetable.useTable
21+
import kotlin.math.ceil
22+
23+
/**
24+
* Example demonstrating useTableRequest hook.
25+
*
26+
* Shows separation of concerns:
27+
* - useTableRequest: Handles data fetching and pagination state
28+
* - useTable: Handles local table features (sorting, filtering)
29+
*/
30+
@Composable
31+
fun UseTableRequestExample() {
32+
// Mock data class
33+
data class User(val id: Int, val name: String, val age: Int)
34+
35+
// Mock API request - returns TableResult directly
36+
suspend fun mockApiRequest(page: Int, pageSize: Int): TableResult<User> {
37+
delay(500) // Simulate network delay
38+
val allUsers = (1..25).map { User(it, "User$it", 20 + it) }
39+
val startIndex = page * pageSize
40+
val endIndex = minOf(startIndex + pageSize, allUsers.size)
41+
val rows = if (startIndex < allUsers.size) {
42+
allUsers.subList(startIndex, endIndex)
43+
} else {
44+
emptyList()
45+
}
46+
return TableResult(rows = rows, total = allUsers.size)
47+
}
48+
49+
// 1. Use useTableRequest for data fetching
50+
val tableRequest = useTableRequest<User>(
51+
requestFn = { page, pageSize -> mockApiRequest(page, pageSize) },
52+
optionsOf = {
53+
initialPageSize = 5
54+
requestOptions = {
55+
onSuccess = { data, _ ->
56+
println("Loaded ${data?.rows?.size ?: 0} users")
57+
}
58+
onError = { error, _ ->
59+
println("Error: ${error.message}")
60+
}
61+
}
62+
}
63+
)
64+
65+
// 2. Extract data for table
66+
val users by tableRequest.rows
67+
val total by tableRequest.total
68+
val loading by tableRequest.isLoading
69+
val currentPage by tableRequest.currentPage
70+
val pageSize by tableRequest.pageSize
71+
72+
// 3. Define columns
73+
val columns = remember {
74+
listOf(
75+
column<User, Int>(
76+
id = "id",
77+
header = "ID",
78+
accessorFn = { it.id }
79+
),
80+
column<User, String>(
81+
id = "name",
82+
header = "Name",
83+
accessorFn = { it.name }
84+
),
85+
column<User, Int>(
86+
id = "age",
87+
header = "Age",
88+
accessorFn = { it.age }
89+
)
90+
)
91+
}
92+
93+
// 4. Use useTable for local table features (sorting only)
94+
val table = useTable(
95+
data = users,
96+
columns = columns
97+
) {
98+
enableSorting = true
99+
}
100+
101+
val tableState by table.state
102+
val rowModel by table.rowModel
103+
val pageCount = remember(total, pageSize) {
104+
if (pageSize <= 0) 1 else ceil(total.toDouble() / pageSize).toInt()
105+
}
106+
107+
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
108+
// Title
109+
Text(
110+
text = "useTableRequest Demo",
111+
style = MaterialTheme.typography.headlineMedium,
112+
fontWeight = FontWeight.Bold
113+
)
114+
115+
Spacer(modifier = Modifier.height(16.dp))
116+
117+
// Controls Area
118+
Row(
119+
modifier = Modifier.fillMaxWidth(),
120+
horizontalArrangement = Arrangement.SpaceBetween,
121+
verticalAlignment = Alignment.CenterVertically
122+
) {
123+
// Loading indicator or row count
124+
if (loading) {
125+
Row(
126+
horizontalArrangement = Arrangement.spacedBy(8.dp),
127+
verticalAlignment = Alignment.CenterVertically
128+
) {
129+
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
130+
Text("Loading...", style = MaterialTheme.typography.bodyMedium)
131+
}
132+
} else {
133+
Text(
134+
"Showing ${rowModel.rows.size} of $total rows",
135+
style = MaterialTheme.typography.bodyMedium
136+
)
137+
}
138+
139+
// Refresh button
140+
IconButton(
141+
onClick = { tableRequest.refresh() },
142+
enabled = !loading
143+
) {
144+
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
145+
}
146+
}
147+
148+
Spacer(modifier = Modifier.height(8.dp))
149+
150+
// Table Header
151+
Card(
152+
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
153+
) {
154+
Row(
155+
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
156+
verticalAlignment = Alignment.CenterVertically
157+
) {
158+
columns.forEach { column ->
159+
val isSorted = tableState.sorting.sorting.any { it.columnId == column.id }
160+
val sortDesc = tableState.sorting.sorting.find { it.columnId == column.id }?.desc
161+
162+
Row(
163+
modifier = Modifier
164+
.weight(1f)
165+
.clickable { table.toggleSorting(column.id, null) }
166+
.padding(vertical = 12.dp),
167+
verticalAlignment = Alignment.CenterVertically
168+
) {
169+
Text(
170+
text = column.header,
171+
style = MaterialTheme.typography.titleMedium,
172+
fontWeight = FontWeight.Bold
173+
)
174+
if (isSorted) {
175+
Spacer(modifier = Modifier.width(4.dp))
176+
Icon(
177+
imageVector = if (sortDesc == true) Icons.Default.KeyboardArrowDown else Icons.Default.KeyboardArrowUp,
178+
contentDescription = "Sort",
179+
modifier = Modifier.size(20.dp)
180+
)
181+
}
182+
}
183+
}
184+
}
185+
}
186+
187+
HorizontalDivider()
188+
189+
// Table Body
190+
LazyColumn(modifier = Modifier.weight(1f)) {
191+
items(rowModel.rows.size) { index ->
192+
val row = rowModel.rows[index]
193+
194+
Surface(
195+
color = if (index % 2 == 0) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
196+
modifier = Modifier.fillMaxWidth()
197+
) {
198+
Row(
199+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
200+
verticalAlignment = Alignment.CenterVertically
201+
) {
202+
Text(
203+
text = row.original.id.toString(),
204+
modifier = Modifier.weight(1f),
205+
style = MaterialTheme.typography.bodyMedium
206+
)
207+
Text(
208+
text = row.original.name,
209+
modifier = Modifier.weight(1f),
210+
style = MaterialTheme.typography.bodyMedium
211+
)
212+
Text(
213+
text = row.original.age.toString(),
214+
modifier = Modifier.weight(1f),
215+
style = MaterialTheme.typography.bodyMedium
216+
)
217+
}
218+
}
219+
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
220+
}
221+
}
222+
223+
Spacer(modifier = Modifier.height(16.dp))
224+
225+
// Pagination Controls
226+
var pageSizeExpanded by remember { mutableStateOf(false) }
227+
var jumpToPageText by remember { mutableStateOf("") }
228+
val pageSizeOptions = listOf(5, 10, 20)
229+
230+
Column(
231+
modifier = Modifier.fillMaxWidth(),
232+
verticalArrangement = Arrangement.spacedBy(12.dp)
233+
) {
234+
// First row: Page size selector and total info
235+
Row(
236+
modifier = Modifier.fillMaxWidth(),
237+
horizontalArrangement = Arrangement.SpaceBetween,
238+
verticalAlignment = Alignment.CenterVertically
239+
) {
240+
// Page size selector
241+
Row(
242+
horizontalArrangement = Arrangement.spacedBy(8.dp),
243+
verticalAlignment = Alignment.CenterVertically
244+
) {
245+
Text("Rows per page:", style = MaterialTheme.typography.bodyMedium)
246+
247+
Box {
248+
OutlinedButton(
249+
onClick = { pageSizeExpanded = true },
250+
modifier = Modifier.width(80.dp)
251+
) {
252+
Text("$pageSize")
253+
Spacer(Modifier.width(4.dp))
254+
Icon(
255+
imageVector = Icons.Default.KeyboardArrowDown,
256+
contentDescription = "Select page size",
257+
modifier = Modifier.size(16.dp)
258+
)
259+
}
260+
261+
DropdownMenu(
262+
expanded = pageSizeExpanded,
263+
onDismissRequest = { pageSizeExpanded = false }
264+
) {
265+
pageSizeOptions.forEach { size ->
266+
DropdownMenuItem(
267+
text = { Text("$size") },
268+
onClick = {
269+
tableRequest.onPageChange(0, size)
270+
pageSizeExpanded = false
271+
}
272+
)
273+
}
274+
}
275+
}
276+
}
277+
278+
// Total rows info
279+
Text("Total: $total rows", style = MaterialTheme.typography.bodyMedium)
280+
}
281+
282+
// Second row: Navigation controls
283+
Row(
284+
modifier = Modifier.fillMaxWidth(),
285+
horizontalArrangement = Arrangement.SpaceBetween,
286+
verticalAlignment = Alignment.CenterVertically
287+
) {
288+
// Previous button
289+
Button(
290+
onClick = { tableRequest.onPageChange(currentPage - 1, pageSize) },
291+
enabled = currentPage > 0 && !loading
292+
) {
293+
Text("Previous")
294+
}
295+
296+
// Page info and jump to page
297+
Row(
298+
horizontalArrangement = Arrangement.spacedBy(8.dp),
299+
verticalAlignment = Alignment.CenterVertically
300+
) {
301+
Text("Page ${currentPage + 1} of $pageCount")
302+
303+
Text("|", color = MaterialTheme.colorScheme.outline)
304+
305+
Text("Go to:", style = MaterialTheme.typography.bodyMedium)
306+
307+
OutlinedTextField(
308+
value = jumpToPageText,
309+
onValueChange = { jumpToPageText = it.filter { char -> char.isDigit() } },
310+
modifier = Modifier.width(70.dp),
311+
singleLine = true,
312+
textStyle = MaterialTheme.typography.bodyMedium
313+
)
314+
315+
Button(
316+
onClick = {
317+
val targetPage = jumpToPageText.toIntOrNull()
318+
if (targetPage != null && targetPage in 1..pageCount) {
319+
tableRequest.onPageChange(targetPage - 1, pageSize)
320+
jumpToPageText = ""
321+
}
322+
},
323+
enabled = jumpToPageText.toIntOrNull()?.let { it in 1..pageCount } == true && !loading
324+
) {
325+
Text("Jump")
326+
}
327+
}
328+
329+
// Next button
330+
Button(
331+
onClick = { tableRequest.onPageChange(currentPage + 1, pageSize) },
332+
enabled = currentPage < pageCount - 1 && !loading
333+
) {
334+
Text("Next")
335+
}
336+
}
337+
}
338+
}
339+
}

app/src/commonMain/kotlin/xyz/junerver/composehooks/route/routes.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import xyz.junerver.composehooks.example.UseSortedExample
4343
import xyz.junerver.composehooks.example.UseStateExample
4444
import xyz.junerver.composehooks.example.UseStateMachineExample
4545
import xyz.junerver.composehooks.example.UseTableExample
46+
import xyz.junerver.composehooks.example.UseTableRequestExample
4647
import xyz.junerver.composehooks.example.UseThrottleExample
4748
import xyz.junerver.composehooks.example.UseTimeAgoExample
4849
import xyz.junerver.composehooks.example.UseTimeoutExample
@@ -134,6 +135,7 @@ val routes = mapOf<String, @Composable () -> Unit>(
134135
"useCycleList" to { UseCycleListExample() },
135136
"useSorted" to { UseSortedExample() },
136137
"useTable" to { UseTableExample() },
138+
"useTableRequest" to { UseTableRequestExample() },
137139
) + platformSpecialRoutes
138140

139141
expect fun getSubRequestRoutes(): Map<String, @Composable () -> Unit>

0 commit comments

Comments
 (0)