Improve state management in FeedTab

This commit is contained in:
Shinokuni 2024-01-17 15:48:07 +01:00
parent 69788de077
commit 215399d3ac
3 changed files with 101 additions and 80 deletions

View File

@ -0,0 +1,51 @@
package com.readrops.app.compose.feeds
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.account.Account
data class FeedState(
val foldersAndFeeds: FolderAndFeedsState = FolderAndFeedsState.InitialState,
val dialog: DialogState? = null,
)
sealed interface DialogState {
object AddFeed : DialogState
class DeleteFeed(val feed: Feed) : DialogState
class UpdateFeed(val feed: Feed) : DialogState
class FeedSheet(val feed: Feed, val folder: Folder?) : DialogState
}
sealed class FolderAndFeedsState {
object InitialState : FolderAndFeedsState()
data class ErrorState(val exception: Exception) : FolderAndFeedsState()
data class LoadedState(val values: Map<Folder?, List<Feed>>) : FolderAndFeedsState()
}
data class AddFeedDialogState(
val url: String = "",
val selectedAccount: Account = Account(accountName = ""),
val accounts: List<Account> = listOf(),
val error: AddFeedError? = null,
val closeDialog: Boolean = false,
) {
fun isError() = error != null
val errorText: String
get() = when (error) {
is AddFeedError.EmptyUrl -> "Field can't be empty"
AddFeedError.BadUrl -> "Input is not a valid URL"
AddFeedError.NoConnection -> ""
AddFeedError.NoRSSFeed -> "No RSS feed found"
AddFeedError.UnreachableUrl -> ""
else -> ""
}
sealed class AddFeedError {
object EmptyUrl : AddFeedError()
object BadUrl : AddFeedError()
object UnreachableUrl : AddFeedError()
object NoRSSFeed : AddFeedError()
object NoConnection : AddFeedError()
}
}

View File

@ -17,9 +17,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@ -33,7 +30,6 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
import com.readrops.app.compose.R import com.readrops.app.compose.R
import com.readrops.app.compose.util.components.Placeholder import com.readrops.app.compose.util.components.Placeholder
import com.readrops.db.entities.Feed import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
object FeedTab : Tab { object FeedTab : Tab {
@ -53,32 +49,31 @@ object FeedTab : Tab {
val viewModel = getViewModel<FeedViewModel>() val viewModel = getViewModel<FeedViewModel>()
val state by viewModel.feedsState.collectAsStateWithLifecycle() val state by viewModel.feedsState.collectAsStateWithLifecycle()
var showDialog by remember { mutableStateOf(false) }
var selectedFeed by remember { mutableStateOf<Feed?>(null) } when (val dialog = state.dialog) {
var selectedFolder by remember { mutableStateOf<Folder?>(null) } is DialogState.AddFeed -> {
var showBottomSheet by remember { mutableStateOf(false) } AddFeedDialog(
viewModel = viewModel,
if (showBottomSheet) { onDismiss = {
FeedModalBottomSheet( viewModel.closeDialog()
feed = selectedFeed!!, viewModel.resetAddFeedDialogState()
folder = selectedFolder, },
onDismissRequest = { showBottomSheet = false }, )
onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) }, }
onModify = { }, is DialogState.DeleteFeed -> {}
onUpdateColor = {}, is DialogState.FeedSheet -> {
onDelete = {}, FeedModalBottomSheet(
) feed = dialog.feed,
} folder = dialog.folder,
onDismissRequest = { viewModel.closeDialog() },
if (showDialog) { onOpen = { uriHandler.openUri(dialog.feed.siteUrl!!) },
AddFeedDialog( onModify = { },
viewModel = viewModel, onUpdateColor = {},
onDismiss = { onDelete = {},
showDialog = false )
viewModel.resetAddFeedDialogState() }
}, is DialogState.UpdateFeed -> {}
) null -> {}
} }
Scaffold( Scaffold(
@ -105,9 +100,9 @@ object FeedTab : Tab {
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
when (state) { when (state.foldersAndFeeds) {
is FeedsState.LoadedState -> { is FolderAndFeedsState.LoadedState -> {
val foldersAndFeeds = (state as FeedsState.LoadedState).foldersAndFeeds val foldersAndFeeds = (state.foldersAndFeeds as FolderAndFeedsState.LoadedState).values
if (foldersAndFeeds.isNotEmpty()) { if (foldersAndFeeds.isNotEmpty()) {
LazyColumn { LazyColumn {
@ -115,12 +110,6 @@ object FeedTab : Tab {
items = foldersAndFeeds.toList() items = foldersAndFeeds.toList()
) { folderWithFeeds -> ) { folderWithFeeds ->
fun onFeedClick(feed: Feed) {
selectedFeed = feed
selectedFolder = folderWithFeeds.first
showBottomSheet = true
}
fun onFeedLongClick(feed: Feed) { fun onFeedLongClick(feed: Feed) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
uriHandler.openUri(feed.siteUrl!!) uriHandler.openUri(feed.siteUrl!!)
@ -130,7 +119,7 @@ object FeedTab : Tab {
FolderExpandableItem( FolderExpandableItem(
folder = folderWithFeeds.first!!, folder = folderWithFeeds.first!!,
feeds = folderWithFeeds.second, feeds = folderWithFeeds.second,
onFeedClick = { feed -> onFeedClick(feed) }, onFeedClick = { feed -> viewModel.openFeedSheet(feed, folderWithFeeds.first) },
onFeedLongClick = { feed -> onFeedLongClick(feed) } onFeedLongClick = { feed -> onFeedLongClick(feed) }
) )
} else { } else {
@ -139,7 +128,7 @@ object FeedTab : Tab {
for (feed in feeds) { for (feed in feeds) {
FeedItem( FeedItem(
feed = feed, feed = feed,
onClick = { onFeedClick(feed) }, onClick = { viewModel.openFeedSheet(feed, null) },
onLongClick = { onFeedLongClick(feed) }, onLongClick = { onFeedLongClick(feed) },
) )
} }
@ -154,7 +143,7 @@ object FeedTab : Tab {
} }
} }
is FeedsState.ErrorState -> { is FolderAndFeedsState.ErrorState -> {
} }
@ -167,7 +156,7 @@ object FeedTab : Tab {
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.padding(16.dp), .padding(16.dp),
onClick = { showDialog = true } onClick = { viewModel.openAddFeedDialog() }
) { ) {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,

View File

@ -27,8 +27,8 @@ class FeedViewModel(
private val localRSSDataSource: LocalRSSDataSource, private val localRSSDataSource: LocalRSSDataSource,
) : TabViewModel(database), KoinComponent { ) : TabViewModel(database), KoinComponent {
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState) private val _feedState = MutableStateFlow(FeedState())
val feedsState = _feedsState.asStateFlow() val feedsState = _feedState.asStateFlow()
private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState()) private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState())
val addFeedDialogState = _addFeedDialogState.asStateFlow() val addFeedDialogState = _addFeedDialogState.asStateFlow()
@ -39,8 +39,16 @@ class FeedViewModel(
.flatMapConcat { account -> .flatMapConcat { account ->
getFoldersWithFeeds.get(account.id) getFoldersWithFeeds.get(account.id)
} }
.catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) } .catch { throwable ->
.collect { _feedsState.value = FeedsState.LoadedState(it) } _feedState.update {
it.copy(foldersAndFeeds = FolderAndFeedsState.ErrorState(Exception(throwable)))
}
}
.collect { foldersAndFeeds ->
_feedState.update {
it.copy(foldersAndFeeds = FolderAndFeedsState.LoadedState(foldersAndFeeds))
}
}
} }
viewModelScope.launch(context = Dispatchers.IO) { viewModelScope.launch(context = Dispatchers.IO) {
@ -57,6 +65,13 @@ class FeedViewModel(
} }
} }
fun closeDialog() = _feedState.update { it.copy(dialog = null) }
fun openAddFeedDialog() = _feedState.update { it.copy(dialog = DialogState.AddFeed) }
fun openFeedSheet(feed: Feed, folder: Folder?) =
_feedState.update { it.copy(dialog = DialogState.FeedSheet(feed, folder)) }
fun setAddFeedDialogURL(url: String) { fun setAddFeedDialogURL(url: String) {
_addFeedDialogState.update { _addFeedDialogState.update {
it.copy( it.copy(
@ -135,37 +150,3 @@ class FeedViewModel(
} }
} }
sealed class FeedsState {
object InitialState : FeedsState()
data class ErrorState(val exception: Exception) : FeedsState()
data class LoadedState(val foldersAndFeeds: Map<Folder?, List<Feed>>) : FeedsState()
}
data class AddFeedDialogState(
val url: String = "",
val selectedAccount: Account = Account(accountName = ""),
val accounts: List<Account> = listOf(),
val error: AddFeedError? = null,
val closeDialog: Boolean = false,
) {
fun isError() = error != null
val errorText: String
get() = when (error) {
is AddFeedError.EmptyUrl -> "Field can't be empty"
AddFeedError.BadUrl -> "Input is not a valid URL"
AddFeedError.NoConnection -> ""
AddFeedError.NoRSSFeed -> "No RSS feed found"
AddFeedError.UnreachableUrl -> ""
else -> ""
}
sealed class AddFeedError {
object EmptyUrl : AddFeedError()
object BadUrl : AddFeedError()
object UnreachableUrl : AddFeedError()
object NoRSSFeed : AddFeedError()
object NoConnection : AddFeedError()
}
}