State hoisting for FlowPage and ReadPage

This commit is contained in:
Ash 2022-04-02 21:40:47 +08:00
parent ac5e68bf86
commit 4c95f89b07
4 changed files with 153 additions and 149 deletions

View File

@ -26,20 +26,20 @@ import me.ash.reader.ui.widget.ViewPager
fun HomePage(
navController: NavHostController,
extrasArticleId: Any? = null,
viewModel: HomeViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) {
val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue()
val filterState = viewModel.filterState.collectAsStateValue()
val syncState = viewModel.syncState.collectAsStateValue()
val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val syncState = homeViewModel.syncState.collectAsStateValue()
OpenArticleByExtras(extrasArticleId)
BackHandler(true) {
val currentPage = viewState.pagerState.currentPage
viewModel.dispatch(
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = when (currentPage) {
@ -58,8 +58,8 @@ fun HomePage(
)
}
LaunchedEffect(viewModel.viewState) {
viewModel.viewState.collect {
LaunchedEffect(homeViewModel.viewState) {
homeViewModel.viewState.collect {
Log.i(
"RLog",
"HomePage: ${it.pagerState.currentPage}, ${it.pagerState.targetPage}, ${it.pagerState.currentPageOffset}"
@ -78,13 +78,13 @@ fun HomePage(
filterState = filterState,
syncState = syncState,
onSyncClick = {
viewModel.dispatch(HomeViewAction.Sync)
homeViewModel.dispatch(HomeViewAction.Sync)
},
onFilterChange = {
viewModel.dispatch(HomeViewAction.ChangeFilter(it))
homeViewModel.dispatch(HomeViewAction.ChangeFilter(it))
},
onScrollToPage = {
viewModel.dispatch(
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = it,
@ -94,10 +94,47 @@ fun HomePage(
)
},
{
FlowPage(navController = navController)
FlowPage(
navController = navController,
filterState = filterState,
onScrollToPage = {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = it,
)
)
},
onFilterChange = {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(it))
},
onItemClick = {
readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
readViewModel.dispatch(ReadViewAction.InitData(it))
if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 2,
)
)
}
)
},
{
ReadPage(navController = navController)
ReadPage(
navController = navController,
onScrollToPage = { targetPage, callback ->
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = targetPage,
callback = callback
),
)
})
},
),
)

View File

@ -13,7 +13,6 @@ import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -25,14 +24,11 @@ import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
import me.ash.reader.ui.widget.LottieAnimation
import me.ash.reader.ui.page.home.FilterState
@OptIn(
ExperimentalMaterial3Api::class,
@ -43,22 +39,21 @@ fun FlowPage(
modifier: Modifier = Modifier,
navController: NavHostController,
flowViewModel: FlowViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
filterState: FilterState,
onFilterChange: (filterState: FilterState) -> Unit = {},
onScrollToPage: (targetPage: Int) -> Unit = {},
onItemClick: (item: ArticleWithFeed) -> Unit = {},
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val viewState = flowViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
var markAsRead by remember { mutableStateOf(false) }
LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state ->
flowViewModel.dispatch(
FlowViewAction.FetchData(state)
)
}
LaunchedEffect(filterState) {
flowViewModel.dispatch(
FlowViewAction.FetchData(filterState)
)
}
// LaunchedEffect(viewState.listState.isScrollInProgress) {
@ -81,14 +76,7 @@ fun FlowPage(
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 0,
)
)
}) {
IconButton(onClick = { onScrollToPage(0) }) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
@ -131,12 +119,14 @@ fun FlowPage(
},
content = {
Crossfade(targetState = pagingItems) { pagingItems ->
if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) {
LottieAnimation(
modifier = Modifier.alpha(0.7f).padding(80.dp),
url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
)
}
// if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) {
// LottieAnimation(
// modifier = Modifier
// .alpha(0.7f)
// .padding(80.dp),
// url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
// )
// }
LazyColumn(
state = viewState.listState,
) {
@ -178,17 +168,7 @@ fun FlowPage(
pagingItems = pagingItems,
) {
markAsRead = false
readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
readViewModel.dispatch(ReadViewAction.InitData(it))
if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 2,
)
)
onItemClick(it)
}
item {
Spacer(modifier = Modifier.height(64.dp))
@ -207,11 +187,9 @@ fun FlowPage(
filter = filterState.filter,
filterOnClick = {
markAsRead = false
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
filter = it
)
onFilterChange(
filterState.copy(
filter = it
)
)
},

View File

@ -1,6 +1,5 @@
package me.ash.reader.ui.page.home.read
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.*
@ -8,49 +7,49 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.formatToString
import me.ash.reader.ui.extension.roundClick
@Composable
fun Header(
context: Context,
article: Article,
feed: Feed
articleWithFeed: ArticleWithFeed,
) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxWidth()
.roundClick {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
Intent(Intent.ACTION_VIEW, Uri.parse(articleWithFeed.article.link))
)
}
.padding(12.dp)
) {
Text(
text = article.date.formatToString(context, atHourMinute = true),
text = articleWithFeed.article.date.formatToString(context, atHourMinute = true),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.title,
text = articleWithFeed.article.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineLarge,
)
Spacer(modifier = Modifier.height(4.dp))
article.author?.let {
articleWithFeed.article.author?.let {
Text(
text = article.author,
text = articleWithFeed.article.author,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
}
Text(
text = feed.name,
text = articleWithFeed.feed.name,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)

View File

@ -1,11 +1,12 @@
package me.ash.reader.ui.page.home.read
import android.content.Context
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Headphones
import androidx.compose.material.icons.outlined.MoreVert
@ -14,20 +15,14 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.CoroutineScope
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.widget.LottieAnimation
import me.ash.reader.ui.widget.WebView
@OptIn(ExperimentalMaterial3Api::class)
@ -36,10 +31,8 @@ fun ReadPage(
navController: NavHostController,
modifier: Modifier = Modifier,
readViewModel: ReadViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
onScrollToPage: (targetPage: Int, callback: () -> Unit) -> Unit = { _, _ -> },
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val viewState = readViewModel.viewState.collectAsStateValue()
var isScrollDown by remember { mutableStateOf(false) }
@ -92,14 +85,18 @@ fun ReadPage(
contentAlignment = Alignment.TopCenter
) {
TopBar(
viewState.articleWithFeed == null || !isScrollDown,
homeViewModel,
scope,
readViewModel,
viewState
isShow = viewState.articleWithFeed == null || !isScrollDown,
onScrollToPage = onScrollToPage,
onClearArticle = {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
)
}
Content(viewState, viewState.articleWithFeed, context)
Content(
content = viewState.content ?: "",
articleWithFeed = viewState.articleWithFeed,
LazyListState = viewState.listState,
)
Box(
modifier = Modifier
.fillMaxSize()
@ -107,9 +104,18 @@ fun ReadPage(
contentAlignment = Alignment.BottomCenter
) {
BottomBar(
viewState.articleWithFeed != null && !isScrollDown,
viewState.articleWithFeed,
readViewModel
isShow = viewState.articleWithFeed != null && !isScrollDown,
articleWithFeed = viewState.articleWithFeed,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
},
starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it))
},
fullContentOnClick = { afterIsFullContent ->
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
},
)
}
}
@ -121,10 +127,9 @@ fun ReadPage(
@Composable
private fun TopBar(
isShow: Boolean,
homeViewModel: HomeViewModel,
scope: CoroutineScope,
readViewModel: ReadViewModel,
viewState: ReadViewState
isShowActions: Boolean = false,
onScrollToPage: (targetPage: Int, callback: () -> Unit) -> Unit = { _, _ -> },
onClearArticle: () -> Unit = {},
) {
AnimatedVisibility(
visible = isShow,
@ -138,15 +143,9 @@ private fun TopBar(
title = {},
navigationIcon = {
IconButton(onClick = {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
callback = {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
)
)
onScrollToPage(1) {
onClearArticle()
}
}) {
Icon(
imageVector = Icons.Rounded.Close,
@ -156,7 +155,7 @@ private fun TopBar(
}
},
actions = {
viewState.articleWithFeed?.let {
if (isShowActions) {
IconButton(onClick = {}) {
Icon(
modifier = Modifier.size(22.dp),
@ -178,60 +177,25 @@ private fun TopBar(
}
}
@Composable
private fun BottomBar(
isShow: Boolean,
articleWithFeed: ArticleWithFeed?,
readViewModel: ReadViewModel
) {
articleWithFeed?.let {
AnimatedVisibility(
visible = isShow,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
ReadBar(
disabled = false,
isUnread = articleWithFeed.article.isUnread,
isStarred = articleWithFeed.article.isStarred,
isFullContent = articleWithFeed.feed.isFullContent,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
},
starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it))
},
fullContentOnClick = { afterIsFullContent ->
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
},
)
}
}
}
@Composable
private fun Content(
viewState: ReadViewState,
content: String,
articleWithFeed: ArticleWithFeed?,
context: Context
LazyListState: LazyListState = rememberLazyListState(),
) {
Column {
if (articleWithFeed == null) {
Spacer(modifier = Modifier.height(64.dp))
LottieAnimation(
modifier = Modifier
.alpha(0.7f)
.padding(80.dp),
url = "https://assets8.lottiefiles.com/packages/lf20_jm7mv1ib.json",
)
// LottieAnimation(
// modifier = Modifier
// .alpha(0.7f)
// .padding(80.dp),
// url = "https://assets8.lottiefiles.com/packages/lf20_jm7mv1ib.json",
// )
} else {
LazyColumn(
state = viewState.listState,
state = LazyListState,
) {
val article = articleWithFeed.article
val feed = articleWithFeed.feed
item {
Spacer(modifier = Modifier.height(64.dp))
}
@ -241,14 +205,14 @@ private fun Content(
modifier = Modifier
.padding(horizontal = 12.dp)
) {
Header(context, article, feed)
Header(articleWithFeed)
}
}
item {
Spacer(modifier = Modifier.height(22.dp))
Crossfade(targetState = viewState.content) { content ->
Crossfade(targetState = content) { content ->
WebView(
content = content ?: "",
content = content
)
Spacer(modifier = Modifier.height(50.dp))
}
@ -262,3 +226,29 @@ private fun Content(
}
}
@Composable
private fun BottomBar(
isShow: Boolean,
articleWithFeed: ArticleWithFeed?,
unreadOnClick: (afterIsUnread: Boolean) -> Unit = {},
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
articleWithFeed?.let {
AnimatedVisibility(
visible = isShow,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
ReadBar(
disabled = false,
isUnread = articleWithFeed.article.isUnread,
isStarred = articleWithFeed.article.isStarred,
isFullContent = articleWithFeed.feed.isFullContent,
unreadOnClick = unreadOnClick,
starredOnClick = starredOnClick,
fullContentOnClick = fullContentOnClick,
)
}
}
}