.opencode/skills/pull-to-refresh/SKILL.md
Implement pull-to-refresh in Jetpack Compose using Material 3 PullToRefreshBox. Use this skill when adding data refresh capability on screens with lists or cards.
npx skillsauth add easydev991/Jetpack-WorkoutApp pull-to-refreshInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Required components:
_isRefreshing) - separate StateFlow for managing loading staterefreshProfile()) - calls data loading from serverImportant rules:
isRefreshing StateFlow instead of main uiStateuiState (use updateUiState = false parameter)_isRefreshing in finally blockRequired components:
Important rules:
PullToRefreshBoxrememberScrollState for verticalScrollisRefreshing from ViewModelCreate separate StateFlow for refresh state:
// Data refresh state (pull-to-refresh)
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
Add a method that updates data from server:
/**
* Refreshes data from server (for pull-to-refresh).
*/
fun refreshData() {
val data = currentData.value ?: run {
logger.w(TAG, "Skipping refresh: data is missing")
return
}
viewModelScope.launch {
try {
_isRefreshing.update { true }
logger.i(TAG, "Starting data refresh: ${data.id}")
// Load data from server
loadDataFromServer(data.id, updateUiState = false)
} catch (e: Exception) {
val errorMessage = "Error refreshing data: ${e.message}"
logger.e(TAG, errorMessage)
} finally {
_isRefreshing.update { false }
}
}
}
Use PullToRefreshBox to wrap content:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(
modifier: Modifier = Modifier,
viewModel: MyViewModel,
) {
val scope = rememberCoroutineScope()
// Get refresh state (isRefreshing)
val isRefreshing by viewModel.isRefreshing.collectAsState()
// State for pull-to-refresh
val pullRefreshState = rememberPullToRefreshState()
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = { viewModel.refreshData() },
state = pullRefreshState,
modifier = modifier.fillMaxSize(),
indicator = {
PullToRefreshDefaults.Indicator(
state = pullRefreshState,
isRefreshing = isRefreshing,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = dimensionResource(R.dimen.spacing_regular))
)
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.spacing_regular)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_small))
) {
// Your content here
}
}
}
For using pull-to-refresh, the following imports are needed:
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
Problem: Data refresh changes main UI State, causing entire screen redraw.
Solution: Use separate isRefreshing StateFlow for managing loading state.
Problem: If error occurs during refresh, isRefreshing state remains true forever.
Solution: Always reset _isRefreshing in finally block.
Problem: Pull-to-refresh doesn't work because content doesn't support scrolling.
Solution: Wrap content in Column with verticalScroll(rememberScrollState()).
Problem: Indicator is overlapped by TopAppBar or other elements.
Solution: Use top = dimensionResource(R.dimen.spacing_regular) for indicator padding.
Test the data refresh method:
@Test
fun refreshData_whenCalled_thenUpdatesIsRefreshing() = runTest {
// Given
mainDispatcherRule.advanceUntilIdle()
// When
viewModel.refreshData()
advanceUntilIdle()
// Then
assertEquals(false, viewModel.isRefreshing.value)
}
testing
Пиши тесты в андроид-проекте правильно. Используй этот навык при написании любых типов тестов (unit, integration, UI), тестировании бизнес-логики, сетевых функций и компонентов UI.
tools
Реализуй pull-to-refresh в Jetpack Compose с использованием Material 3 PullToRefreshBox. Используй этот навык при добавлении возможности обновления данных на экранах со списками или карточками.
tools
Правильно работай с локализацией в Android-проекте. Используй этот навык при добавлении новых строковых ресурсов, работе с plurals, форматировании дат и текстов.
tools
Блокировка UI контента экрана во время загрузки данных или выполнения асинхронных операций с отображением индикатора загрузки. Используй этот навык при разработке экранов с сетевыми запросами, операциями CRUD и других асинхронных действий.