I'm using the Paging 3 library with Jetpack Compose and have just implemented swipe to dismiss on some paged data (using the Material library's SwipeToDismiss
composable).
Once a swipe action has completed, I call a method in my ViewModel to send an update to the server (either to mark a message as read or to delete a message). Once this action has taken place, I obviously need to refresh the paging data.
My current approach is to have a call back from my ViewModel function which will then handle the refresh on the LazyPagingItems
, but this feels wrong.
Is there a better approach?
My ViewModel basically looks like:
@HiltViewModel
class MessageListViewModel @Inject constructor(
private val repository: Repository
): ViewModel() {
companion object {
private const val TAG = "MessageListViewModel"
}
val messages : Flow<PagingData<Message>> = Pager(
PagingConfig(
enablePlaceholders = false,
)
) {
MessagePagingSource(repository)
}.flow.cachedIn(viewModelScope)
fun markRead(guid: String, onComplete: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.markMessageRead(guid)
onComplete()
} catch (e: Throwable) {
Log.e(TAG, "Error marking message read: $guid", e)
}
}
}
}
And in my Composable for the message list, it looks a bit like the following:
@Composable
fun MessageListScreen(
vm: MessageListViewModel = viewModel(),
) {
val messages: LazyPagingItems<MessageSummary> = vm.messages.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(
isRefreshing = messages.loadState.refresh is LoadState.Loading,
)
Scaffold(
topBar = {
SmallTopAppBar (
title = {
Text(stringResource(R.string.message_list_title))
},
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
) {
SwipeRefresh(
state = refreshState,
onRefresh = {
messages.refresh()
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
items(
items = messages,
key = { it.guid }
) { message ->
message?.let {
MessageRow(
onMarkRead = {
vm.markRead(message.guid) {
messages.refresh()
}
},
)
}
}
}
}
}
}
}
As I say, this does work, it just doesn't quite feel like the cleanest approach.
I'm fairly new to working with flows, so I don't know if there's some other trick I'm missing...
I ended up implementing something like this:
View Model:
class MessageListViewModel @Inject constructor(
private val repository: Repository,
): ViewModel() {
sealed class UiAction {
class MarkReadError(val error: Throwable): UiAction()
class MarkedRead(val id: Long): UiAction()
}
private val _uiActions = MutableSharedFlow<UiAction>()
val uiActions = _uiActions.asSharedFlow()
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
)
fun markRead(id: Long) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.markMessageRead(id)
_uiActions.emit(UiAction.MarkedRead(id))
} catch (e: Throwable) {
Log.e(TAG, "Error marking message read: $id", e)
_uiActions.emit(UiAction.MarkReadError(e))
}
}
}
}
View:
@Composable
fun MessageListScreen(
vm: MessageListViewModel = viewModel(),
onMarkReadFailed: (String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val messages: LazyPagingItems<Message> = vm.messages.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(
isRefreshing = messages.loadState.refresh is LoadState.Loading,
)
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.uiActions.collectLatest {
when (it) {
is MessageListViewModel.UiAction.MarkReadError -> {
val msg = it.error.localizedMessage ?: it.error.message
val message = if (!msg.isNullOrEmpty()) {
context.getString(R.string.error_unknown_error_with_message, msg)
} else {
context.getString(R.string.error_unknown_error_without_message)
}
onMarkReadFailed(message)
}
is MessageListViewModel.UiAction.MarkedRead -> {
messages.refresh()
}
}
}
}
}
SwipeRefresh(
state = refreshState,
onRefresh = {
messages.refresh()
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
state = listState,
) {
items(
items = messages,
key = { it.id }
) { message ->
message?.let {
MessageRow(
onMarkRead = {
vm.markRead(message.id)
},
)
}
FadedDivider()
}
messages.apply {
when (loadState.append) {
is LoadState.Loading -> {
item {
LoadingRow(R.string.messages_loading)
}
}
else -> {}
}
}
}
}
}