From 4c95f89b07810fb2eb0297da3393d0ea6a0e3e5a Mon Sep 17 00:00:00 2001 From: Ash Date: Sat, 2 Apr 2022 21:40:47 +0800 Subject: [PATCH] State hoisting for FlowPage and ReadPage --- .../me/ash/reader/ui/page/home/HomePage.kt | 61 +++++-- .../ash/reader/ui/page/home/flow/FlowPage.kt | 68 +++----- .../me/ash/reader/ui/page/home/read/Header.kt | 23 ++- .../ash/reader/ui/page/home/read/ReadPage.kt | 150 ++++++++---------- 4 files changed, 153 insertions(+), 149 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt index 965fb6e..7d89de0 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt @@ -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 + ), + ) + }) }, ), ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index 2d94cd6..bd82921 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -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 ) ) }, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt index 24f9b25..fee1897 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt @@ -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, ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt index 0a12030..98d6a78 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt @@ -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, + ) + } + } +} \ No newline at end of file