Skip to content

Commit f67d459

Browse files
adalpariclaude
andcommitted
Add pagination to Subscribers and Emails detail pages
Detail pages now fetch data via their own ViewModels with infinite scroll instead of receiving all items through Intent extras. Subscribers uses page-based pagination; Emails uses increasing quantity. Both use mutex- guarded loading with scroll-to-end detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8189f9a commit f67d459

File tree

15 files changed

+1119
-193
lines changed

15 files changed

+1119
-193
lines changed

WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,13 @@ interface StatsDataSource {
229229
*
230230
* @param siteId The WordPress.com site ID
231231
* @param perPage Number of subscribers per page
232+
* @param page Page number (1-based)
232233
* @return Result containing subscriber items or an error
233234
*/
234235
suspend fun fetchSubscribersByUserType(
235236
siteId: Long,
236-
perPage: Int = 10
237+
perPage: Int = 10,
238+
page: Int = 1
237239
): SubscribersByUserTypeDataResult
238240

239241
/**

WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,12 +1053,13 @@ class StatsDataSourceImpl @Inject constructor(
10531053

10541054
override suspend fun fetchSubscribersByUserType(
10551055
siteId: Long,
1056-
perPage: Int
1056+
perPage: Int,
1057+
page: Int
10571058
): SubscribersByUserTypeDataResult {
10581059
val params = SubscribersByUserTypeParams(
10591060
userType = SubscribersByUserTypeUserType.WP_COM,
10601061
perPage = perPage.toULong(),
1061-
page = 1uL,
1062+
page = page.toULong(),
10621063
sort = SubscribersByUserTypeSortField
10631064
.DATE_SUBSCRIBED
10641065
)

WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,11 +1463,14 @@ class StatsRepository @Inject constructor(
14631463
*/
14641464
suspend fun fetchSubscribersList(
14651465
siteId: Long,
1466-
perPage: Int = SUBSCRIBERS_DEFAULT_MAX
1466+
perPage: Int = SUBSCRIBERS_DEFAULT_MAX,
1467+
page: Int = 1
14671468
): SubscribersListResult = withContext(ioDispatcher) {
14681469
when (
14691470
val result = statsDataSource
1470-
.fetchSubscribersByUserType(siteId, perPage)
1471+
.fetchSubscribersByUserType(
1472+
siteId, perPage, page
1473+
)
14711474
) {
14721475
is SubscribersByUserTypeDataResult.Success -> {
14731476
SubscribersListResult.Success(

WordPress/src/main/java/org/wordpress/android/ui/newstats/subscribers/SubscribersTabContent.kt

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import androidx.compose.runtime.collectAsState
3232
import androidx.compose.runtime.getValue
3333
import androidx.compose.runtime.mutableStateOf
3434
import androidx.compose.runtime.remember
35-
import androidx.compose.runtime.rememberCoroutineScope
3635
import androidx.compose.runtime.setValue
3736
import androidx.compose.ui.Alignment
3837
import androidx.compose.ui.Modifier
@@ -44,7 +43,6 @@ import androidx.compose.ui.semantics.semantics
4443
import androidx.compose.ui.text.style.TextAlign
4544
import androidx.compose.ui.unit.dp
4645
import androidx.lifecycle.viewmodel.compose.viewModel
47-
import kotlinx.coroutines.launch
4846
import org.wordpress.android.R
4947
import org.wordpress.android.ui.newstats.components.CardPosition
5048
import org.wordpress.android.ui.newstats.subscribers.alltimestats.AllTimeSubscribersCard
@@ -73,7 +71,6 @@ fun SubscribersTabContent(
7371
viewModel()
7472
) {
7573
val context = LocalContext.current
76-
val scope = rememberCoroutineScope()
7774

7875
val allTimeUiState by
7976
allTimeViewModel.uiState.collectAsState()
@@ -351,13 +348,8 @@ fun SubscribersTabContent(
351348
uiState =
352349
subscribersListUiState,
353350
onShowAllClick = {
354-
scope.launch {
355-
val items =
356-
subscribersListViewModel
357-
.getDetailData()
358-
SubscribersListDetailActivity
359-
.start(context, items)
360-
}
351+
SubscribersListDetailActivity
352+
.start(context)
361353
},
362354
onRetry = {
363355
subscribersListViewModel
@@ -396,15 +388,8 @@ fun SubscribersTabContent(
396388
EmailsCard(
397389
uiState = emailsUiState,
398390
onShowAllClick = {
399-
scope.launch {
400-
val items =
401-
emailsViewModel
402-
.getDetailData()
403-
EmailsDetailActivity
404-
.start(
405-
context, items
406-
)
407-
}
391+
EmailsDetailActivity
392+
.start(context)
408393
},
409394
onRetry = {
410395
emailsViewModel

WordPress/src/main/java/org/wordpress/android/ui/newstats/subscribers/emails/EmailsCardViewModel.kt

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import org.wordpress.android.viewmodel.ResourceProvider
1010
import javax.inject.Inject
1111

1212
private const val CARD_MAX_ITEMS = 5
13-
private const val DETAIL_MAX_ITEMS = 100
1413

1514
@HiltViewModel
1615
class EmailsCardViewModel @Inject constructor(
@@ -32,24 +31,6 @@ class EmailsCardViewModel @Inject constructor(
3231
isAuthError: Boolean
3332
) = EmailsCardUiState.Error(message, isAuthError)
3433

35-
suspend fun getDetailData(): List<EmailListItem> {
36-
val siteId = getSiteId() ?: return emptyList()
37-
val result = statsRepository.fetchEmailsSummary(
38-
siteId, DETAIL_MAX_ITEMS
39-
)
40-
return when (result) {
41-
is EmailsStatsResult.Success ->
42-
result.items.map {
43-
EmailListItem(
44-
title = it.title,
45-
opens = it.opens,
46-
clicks = it.clicks
47-
)
48-
}
49-
is EmailsStatsResult.Error -> emptyList()
50-
}
51-
}
52-
5334
override suspend fun loadDataInternal(siteId: Long) {
5435
when (
5536
val result = statsRepository

WordPress/src/main/java/org/wordpress/android/ui/newstats/subscribers/emails/EmailsDetailActivity.kt

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ import android.content.Context
44
import android.content.Intent
55
import android.os.Bundle
66
import androidx.activity.compose.setContent
7+
import androidx.compose.foundation.layout.Box
78
import androidx.compose.foundation.layout.Row
89
import androidx.compose.foundation.layout.Spacer
910
import androidx.compose.foundation.layout.fillMaxSize
1011
import androidx.compose.foundation.layout.fillMaxWidth
1112
import androidx.compose.foundation.layout.height
1213
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.size
1315
import androidx.compose.foundation.layout.width
1416
import androidx.compose.foundation.lazy.LazyColumn
1517
import androidx.compose.foundation.lazy.itemsIndexed
18+
import androidx.compose.foundation.lazy.rememberLazyListState
1619
import androidx.compose.material.icons.Icons
1720
import androidx.compose.material.icons.automirrored.filled.ArrowBack
21+
import androidx.compose.material3.CircularProgressIndicator
1822
import androidx.compose.material3.ExperimentalMaterial3Api
1923
import androidx.compose.material3.HorizontalDivider
2024
import androidx.compose.material3.Icon
@@ -24,37 +28,34 @@ import androidx.compose.material3.Scaffold
2428
import androidx.compose.material3.Text
2529
import androidx.compose.material3.TopAppBar
2630
import androidx.compose.runtime.Composable
31+
import androidx.compose.runtime.LaunchedEffect
32+
import androidx.compose.runtime.collectAsState
33+
import androidx.compose.runtime.derivedStateOf
34+
import androidx.compose.runtime.getValue
35+
import androidx.compose.runtime.remember
2736
import androidx.compose.ui.Alignment
2837
import androidx.compose.ui.Modifier
2938
import androidx.compose.ui.res.stringResource
3039
import androidx.compose.ui.text.font.FontWeight
3140
import androidx.compose.ui.text.style.TextAlign
3241
import androidx.compose.ui.text.style.TextOverflow
3342
import androidx.compose.ui.unit.dp
43+
import androidx.lifecycle.viewmodel.compose.viewModel
3444
import dagger.hilt.android.AndroidEntryPoint
3545
import org.wordpress.android.R
3646
import org.wordpress.android.ui.compose.theme.AppThemeM3
3747
import org.wordpress.android.ui.main.BaseAppCompatActivity
3848
import org.wordpress.android.ui.newstats.util.formatEmailStat
39-
import org.wordpress.android.ui.newstats.util.formatStatValue
40-
import org.wordpress.android.util.extensions.getParcelableArrayListCompat
4149

42-
private const val EXTRA_ITEMS = "extra_items"
50+
private const val LOAD_MORE_THRESHOLD = 5
4351

4452
@AndroidEntryPoint
4553
class EmailsDetailActivity : BaseAppCompatActivity() {
4654
override fun onCreate(savedInstanceState: Bundle?) {
4755
super.onCreate(savedInstanceState)
48-
49-
val items = intent.extras
50-
?.getParcelableArrayListCompat<EmailListItem>(
51-
EXTRA_ITEMS
52-
) ?: arrayListOf()
53-
5456
setContent {
5557
AppThemeM3 {
5658
EmailsDetailScreen(
57-
items = items,
5859
onBackPressed =
5960
onBackPressedDispatcher::onBackPressed
6061
)
@@ -63,18 +64,11 @@ class EmailsDetailActivity : BaseAppCompatActivity() {
6364
}
6465

6566
companion object {
66-
fun start(
67-
context: Context,
68-
items: List<EmailListItem>
69-
) {
67+
fun start(context: Context) {
7068
val intent = Intent(
7169
context,
7270
EmailsDetailActivity::class.java
73-
).apply {
74-
putExtra(
75-
EXTRA_ITEMS, ArrayList(items)
76-
)
77-
}
71+
)
7872
context.startActivity(intent)
7973
}
8074
}
@@ -83,9 +77,40 @@ class EmailsDetailActivity : BaseAppCompatActivity() {
8377
@OptIn(ExperimentalMaterial3Api::class)
8478
@Composable
8579
private fun EmailsDetailScreen(
86-
items: List<EmailListItem>,
80+
viewModel: EmailsDetailViewModel = viewModel(),
8781
onBackPressed: () -> Unit
8882
) {
83+
val items by viewModel.items.collectAsState()
84+
val isLoading by viewModel.isLoading.collectAsState()
85+
val isLoadingMore by
86+
viewModel.isLoadingMore.collectAsState()
87+
val canLoadMore by
88+
viewModel.canLoadMore.collectAsState()
89+
90+
val listState = rememberLazyListState()
91+
92+
LaunchedEffect(Unit) {
93+
viewModel.loadInitialPage()
94+
}
95+
96+
val shouldLoadMore by remember {
97+
derivedStateOf {
98+
val lastVisible =
99+
listState.layoutInfo.visibleItemsInfo
100+
.lastOrNull()?.index ?: 0
101+
val totalItems =
102+
listState.layoutInfo.totalItemsCount
103+
canLoadMore && !isLoadingMore &&
104+
totalItems > 0 &&
105+
lastVisible >= totalItems -
106+
LOAD_MORE_THRESHOLD
107+
}
108+
}
109+
110+
LaunchedEffect(shouldLoadMore) {
111+
if (shouldLoadMore) viewModel.loadMore()
112+
}
113+
89114
val title = stringResource(
90115
R.string.stats_subscribers_emails
91116
)
@@ -95,7 +120,9 @@ private fun EmailsDetailScreen(
95120
TopAppBar(
96121
title = { Text(text = title) },
97122
navigationIcon = {
98-
IconButton(onClick = onBackPressed) {
123+
IconButton(
124+
onClick = onBackPressed
125+
) {
99126
Icon(
100127
Icons.AutoMirrored.Filled
101128
.ArrowBack,
@@ -107,29 +134,66 @@ private fun EmailsDetailScreen(
107134
)
108135
}
109136
) { contentPadding ->
110-
LazyColumn(
111-
modifier = Modifier
112-
.fillMaxSize()
113-
.padding(contentPadding)
114-
.padding(horizontal = 16.dp)
115-
) {
116-
item {
117-
Spacer(modifier = Modifier.height(8.dp))
118-
DetailEmailColumnHeaders()
119-
Spacer(modifier = Modifier.height(8.dp))
137+
if (isLoading) {
138+
Box(
139+
modifier = Modifier
140+
.fillMaxSize()
141+
.padding(contentPadding),
142+
contentAlignment = Alignment.Center
143+
) {
144+
CircularProgressIndicator()
120145
}
121-
122-
itemsIndexed(items) { index, item ->
123-
DetailEmailRow(item = item)
124-
if (index < items.lastIndex) {
146+
} else {
147+
LazyColumn(
148+
state = listState,
149+
modifier = Modifier
150+
.fillMaxSize()
151+
.padding(contentPadding)
152+
.padding(horizontal = 16.dp)
153+
) {
154+
item {
155+
Spacer(
156+
modifier = Modifier.height(8.dp)
157+
)
158+
DetailEmailColumnHeaders()
125159
Spacer(
126-
modifier = Modifier.height(4.dp)
160+
modifier = Modifier.height(8.dp)
127161
)
128162
}
129-
}
130163

131-
item {
132-
Spacer(modifier = Modifier.height(16.dp))
164+
itemsIndexed(items) { index, item ->
165+
DetailEmailRow(item = item)
166+
if (index < items.lastIndex) {
167+
Spacer(
168+
modifier =
169+
Modifier.height(4.dp)
170+
)
171+
}
172+
}
173+
174+
if (isLoadingMore) {
175+
item {
176+
Box(
177+
modifier = Modifier
178+
.fillMaxWidth()
179+
.padding(16.dp),
180+
contentAlignment =
181+
Alignment.Center
182+
) {
183+
CircularProgressIndicator(
184+
modifier =
185+
Modifier.size(24.dp),
186+
strokeWidth = 2.dp
187+
)
188+
}
189+
}
190+
}
191+
192+
item {
193+
Spacer(
194+
modifier = Modifier.height(16.dp)
195+
)
196+
}
133197
}
134198
}
135199
}

0 commit comments

Comments
 (0)