Audit: PullToRefreshBox on remaining list screens (iOS parity)

HomeScreen + AllTasksScreen + TasksScreen now support pull-to-refresh.
forceRefresh=true per CLAUDE.md mutation pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 17:50:27 -05:00
parent 95f7318ee6
commit d8569c7aed
3 changed files with 105 additions and 48 deletions

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -49,6 +50,7 @@ fun AllTasksScreen(
var showCompleteDialog by remember { mutableStateOf(false) } var showCompleteDialog by remember { mutableStateOf(false) }
var showNewTaskDialog by remember { mutableStateOf(false) } var showNewTaskDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) } var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
var isRefreshing by remember { mutableStateOf(false) }
// Track which column to scroll to (from push notification navigation) // Track which column to scroll to (from push notification navigation)
var scrollToColumnIndex by remember { mutableStateOf<Int?>(null) } var scrollToColumnIndex by remember { mutableStateOf<Int?>(null) }
@@ -58,6 +60,13 @@ fun AllTasksScreen(
residenceViewModel.loadMyResidences() residenceViewModel.loadMyResidences()
} }
// Reset pull-to-refresh state once tasks finish loading
LaunchedEffect(tasksState) {
if (tasksState !is ApiResult.Loading) {
isRefreshing = false
}
}
// When tasks load and we have a pending navigation, find the column containing the task // When tasks load and we have a pending navigation, find the column containing the task
LaunchedEffect(navigateToTaskId, tasksState) { LaunchedEffect(navigateToTaskId, tasksState) {
if (navigateToTaskId != null && tasksState is ApiResult.Success) { if (navigateToTaskId != null && tasksState is ApiResult.Success) {
@@ -206,62 +215,71 @@ fun AllTasksScreen(
} }
} }
} else { } else {
DynamicTaskKanbanView( PullToRefreshBox(
columns = taskData.columns, isRefreshing = isRefreshing,
onCompleteTask = { task -> onRefresh = {
if (onNavigateToCompleteTask != null) { isRefreshing = true
// Use full-screen navigation viewModel.loadTasks(forceRefresh = true)
val residenceName = (myResidencesState as? ApiResult.Success)
?.data?.residences?.find { it.id == task.residenceId }?.name ?: ""
onNavigateToCompleteTask(task, residenceName)
} else {
// Fall back to dialog
selectedTask = task
showCompleteDialog = true
}
}, },
onEditTask = { task -> modifier = Modifier.fillMaxSize()
onNavigateToEditTask(task) ) {
}, DynamicTaskKanbanView(
onCancelTask = { task -> columns = taskData.columns,
onCompleteTask = { task ->
if (onNavigateToCompleteTask != null) {
// Use full-screen navigation
val residenceName = (myResidencesState as? ApiResult.Success)
?.data?.residences?.find { it.id == task.residenceId }?.name ?: ""
onNavigateToCompleteTask(task, residenceName)
} else {
// Fall back to dialog
selectedTask = task
showCompleteDialog = true
}
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
// viewModel.cancelTask(task.id) { _ -> // viewModel.cancelTask(task.id) { _ ->
// viewModel.loadTasks() // viewModel.loadTasks()
// } // }
}, },
onUncancelTask = { task -> onUncancelTask = { task ->
// viewModel.uncancelTask(task.id) { _ -> // viewModel.uncancelTask(task.id) { _ ->
// viewModel.loadTasks() // viewModel.loadTasks()
// } // }
}, },
onMarkInProgress = { task -> onMarkInProgress = { task ->
viewModel.markInProgress(task.id) { success -> viewModel.markInProgress(task.id) { success ->
if (success) { if (success) {
viewModel.loadTasks() viewModel.loadTasks()
}
} }
} },
}, onArchiveTask = { task ->
onArchiveTask = { task -> viewModel.archiveTask(task.id) { success ->
viewModel.archiveTask(task.id) { success -> if (success) {
if (success) { viewModel.loadTasks()
viewModel.loadTasks() }
} }
} },
}, onUnarchiveTask = { task ->
onUnarchiveTask = { task -> viewModel.unarchiveTask(task.id) { success ->
viewModel.unarchiveTask(task.id) { success -> if (success) {
if (success) { viewModel.loadTasks()
viewModel.loadTasks() }
} }
},
modifier = Modifier,
bottomPadding = bottomNavBarPadding,
scrollToColumnIndex = scrollToColumnIndex,
onScrollComplete = {
scrollToColumnIndex = null
onClearNavigateToTask()
} }
}, )
modifier = Modifier, }
bottomPadding = bottomNavBarPadding,
scrollToColumnIndex = scrollToColumnIndex,
onScrollComplete = {
scrollToColumnIndex = null
onClearNavigateToTask()
}
)
} }
} }
} }

View File

@@ -2,9 +2,12 @@ package com.tt.honeyDue.ui.screens
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -32,6 +35,7 @@ fun HomeScreen(
) { ) {
val summaryState by viewModel.myResidencesState.collectAsStateWithLifecycle() val summaryState by viewModel.myResidencesState.collectAsStateWithLifecycle()
val totalSummary by DataManager.totalSummary.collectAsStateWithLifecycle() val totalSummary by DataManager.totalSummary.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.loadMyResidences() viewModel.loadMyResidences()
@@ -39,6 +43,13 @@ fun HomeScreen(
taskViewModel.loadTasks() taskViewModel.loadTasks()
} }
// Reset refresh state once the underlying load completes
LaunchedEffect(summaryState) {
if (summaryState !is ApiResult.Loading) {
isRefreshing = false
}
}
// Handle errors for loading summary // Handle errors for loading summary
summaryState.HandleErrors( summaryState.HandleErrors(
onRetry = { viewModel.loadMyResidences() }, onRetry = { viewModel.loadMyResidences() },
@@ -65,10 +76,21 @@ fun HomeScreen(
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground { WarmGradientBackground {
Column( PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadMyResidences(forceRefresh = true)
taskViewModel.loadTasks(forceRefresh = true)
},
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy), .padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous)
) { ) {
@@ -199,6 +221,7 @@ fun HomeScreen(
onClick = onNavigateToTasks onClick = onNavigateToTasks
) )
} }
}
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -43,6 +44,7 @@ fun TasksScreen(
var selectedTask by remember { mutableStateOf<com.tt.honeyDue.models.TaskDetail?>(null) } var selectedTask by remember { mutableStateOf<com.tt.honeyDue.models.TaskDetail?>(null) }
var showErrorDialog by remember { mutableStateOf(false) } var showErrorDialog by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") } var errorMessage by remember { mutableStateOf("") }
var isRefreshing by remember { mutableStateOf(false) }
// Show error dialog when tasks fail to load // Show error dialog when tasks fail to load
LaunchedEffect(tasksState) { LaunchedEffect(tasksState) {
@@ -50,6 +52,9 @@ fun TasksScreen(
errorMessage = com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message) errorMessage = com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)
showErrorDialog = true showErrorDialog = true
} }
if (tasksState !is ApiResult.Loading) {
isRefreshing = false
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -138,12 +143,22 @@ fun TasksScreen(
} }
} }
} else { } else {
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadTasks(forceRefresh = true)
},
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy, top = OrganicSpacing.cozy,
bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy, bottom = OrganicSpacing.cozy,
start = OrganicSpacing.cozy, start = OrganicSpacing.cozy,
end = OrganicSpacing.cozy end = OrganicSpacing.cozy
), ),
@@ -261,6 +276,7 @@ fun TasksScreen(
} }
} }
} }
}
} }
} }