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( fun HomePage(
navController: NavHostController, navController: NavHostController,
extrasArticleId: Any? = null, extrasArticleId: Any? = null,
viewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(),
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue() val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = viewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
val syncState = viewModel.syncState.collectAsStateValue() val syncState = homeViewModel.syncState.collectAsStateValue()
OpenArticleByExtras(extrasArticleId) OpenArticleByExtras(extrasArticleId)
BackHandler(true) { BackHandler(true) {
val currentPage = viewState.pagerState.currentPage val currentPage = viewState.pagerState.currentPage
viewModel.dispatch( homeViewModel.dispatch(
HomeViewAction.ScrollToPage( HomeViewAction.ScrollToPage(
scope = scope, scope = scope,
targetPage = when (currentPage) { targetPage = when (currentPage) {
@ -58,8 +58,8 @@ fun HomePage(
) )
} }
LaunchedEffect(viewModel.viewState) { LaunchedEffect(homeViewModel.viewState) {
viewModel.viewState.collect { homeViewModel.viewState.collect {
Log.i( Log.i(
"RLog", "RLog",
"HomePage: ${it.pagerState.currentPage}, ${it.pagerState.targetPage}, ${it.pagerState.currentPageOffset}" "HomePage: ${it.pagerState.currentPage}, ${it.pagerState.targetPage}, ${it.pagerState.currentPageOffset}"
@ -78,13 +78,13 @@ fun HomePage(
filterState = filterState, filterState = filterState,
syncState = syncState, syncState = syncState,
onSyncClick = { onSyncClick = {
viewModel.dispatch(HomeViewAction.Sync) homeViewModel.dispatch(HomeViewAction.Sync)
}, },
onFilterChange = { onFilterChange = {
viewModel.dispatch(HomeViewAction.ChangeFilter(it)) homeViewModel.dispatch(HomeViewAction.ChangeFilter(it))
}, },
onScrollToPage = { onScrollToPage = {
viewModel.dispatch( homeViewModel.dispatch(
HomeViewAction.ScrollToPage( HomeViewAction.ScrollToPage(
scope = scope, scope = scope,
targetPage = it, 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -25,14 +24,11 @@ import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R 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.collectAsStateValue
import me.ash.reader.ui.extension.getName import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.FilterState
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
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@ -43,23 +39,22 @@ fun FlowPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navController: NavHostController, navController: NavHostController,
flowViewModel: FlowViewModel = hiltViewModel(), flowViewModel: FlowViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(), filterState: FilterState,
readViewModel: ReadViewModel = hiltViewModel(), onFilterChange: (filterState: FilterState) -> Unit = {},
onScrollToPage: (targetPage: Int) -> Unit = {},
onItemClick: (item: ArticleWithFeed) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = flowViewModel.viewState.collectAsStateValue() val viewState = flowViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems() val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
var markAsRead by remember { mutableStateOf(false) } var markAsRead by remember { mutableStateOf(false) }
LaunchedEffect(homeViewModel.filterState) { LaunchedEffect(filterState) {
homeViewModel.filterState.collect { state ->
flowViewModel.dispatch( flowViewModel.dispatch(
FlowViewAction.FetchData(state) FlowViewAction.FetchData(filterState)
) )
} }
}
// LaunchedEffect(viewState.listState.isScrollInProgress) { // LaunchedEffect(viewState.listState.isScrollInProgress) {
// Log.i("RLog", "isScrollInProgress: ${viewState.listState.isScrollInProgress}") // Log.i("RLog", "isScrollInProgress: ${viewState.listState.isScrollInProgress}")
@ -81,14 +76,7 @@ fun FlowPage(
SmallTopAppBar( SmallTopAppBar(
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(onClick = { onScrollToPage(0) }) {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 0,
)
)
}) {
Icon( Icon(
imageVector = Icons.Rounded.ArrowBack, imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back), contentDescription = stringResource(R.string.back),
@ -131,12 +119,14 @@ fun FlowPage(
}, },
content = { content = {
Crossfade(targetState = pagingItems) { pagingItems -> Crossfade(targetState = pagingItems) { pagingItems ->
if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) { // if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) {
LottieAnimation( // LottieAnimation(
modifier = Modifier.alpha(0.7f).padding(80.dp), // modifier = Modifier
url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json", // .alpha(0.7f)
) // .padding(80.dp),
} // url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
// )
// }
LazyColumn( LazyColumn(
state = viewState.listState, state = viewState.listState,
) { ) {
@ -178,17 +168,7 @@ fun FlowPage(
pagingItems = pagingItems, pagingItems = pagingItems,
) { ) {
markAsRead = false markAsRead = false
readViewModel.dispatch(ReadViewAction.ScrollToItem(0)) onItemClick(it)
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,
)
)
} }
item { item {
Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(64.dp))
@ -207,13 +187,11 @@ fun FlowPage(
filter = filterState.filter, filter = filterState.filter,
filterOnClick = { filterOnClick = {
markAsRead = false markAsRead = false
homeViewModel.dispatch( onFilterChange(
HomeViewAction.ChangeFilter(
filterState.copy( filterState.copy(
filter = it filter = it
) )
) )
)
}, },
) )
} }

View File

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

View File

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