feat: 新版搜索状态屏

This commit is contained in:
HuanCheng65 2023-09-23 15:00:32 +08:00
parent 23f73f2e1a
commit 5f7203228e
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
9 changed files with 308 additions and 194 deletions

View File

@ -72,6 +72,8 @@ import androidx.compose.ui.util.fastForEachIndexed
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.emitGlobalEvent import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.emitGlobalEventSuspend
import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.models.database.SearchHistory import com.huanchengfly.tieba.post.models.database.SearchHistory
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@ -134,12 +136,17 @@ fun SearchPage(
derivedStateOf { keyword.isEmpty() } derivedStateOf { keyword.isEmpty() }
} }
var inputKeyword by remember { mutableStateOf("") } var inputKeyword by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
val initialSortType = remember { SearchThreadSortType.SORT_TYPE_NEWEST } val initialSortType = remember { SearchThreadSortType.SORT_TYPE_NEWEST }
var searchThreadSortType by remember { mutableIntStateOf(initialSortType) } var searchThreadSortType by remember { mutableIntStateOf(initialSortType) }
LaunchedEffect(searchThreadSortType) { LaunchedEffect(searchThreadSortType) {
emitGlobalEvent(SearchThreadUiEvent.SwitchSortType(searchThreadSortType)) emitGlobalEvent(SearchThreadUiEvent.SwitchSortType(searchThreadSortType))
} }
viewModel.onEvent<SearchUiEvent.KeywordChanged> {
inputKeyword = it.keyword
emitGlobalEventSuspend(it)
}
val pages by remember { val pages by remember {
derivedStateOf { derivedStateOf {
@ -234,6 +241,8 @@ fun SearchPage(
inputKeyword = it.content inputKeyword = it.content
viewModel.send(SearchUiIntent.SubmitKeyword(it.content)) viewModel.send(SearchUiIntent.SubmitKeyword(it.content))
}, },
expanded = expanded,
onToggleExpand = { expanded = !expanded },
onClear = { viewModel.send(SearchUiIntent.ClearSearchHistory) } onClear = { viewModel.send(SearchUiIntent.ClearSearchHistory) }
) )
} }

View File

@ -47,6 +47,8 @@ class SearchViewModel :
App.INSTANCE.getString(R.string.toast_clear_failure, partialChange.errorMessage) App.INSTANCE.getString(R.string.toast_clear_failure, partialChange.errorMessage)
) )
is SearchPartialChange.SubmitKeyword -> SearchUiEvent.KeywordChanged(partialChange.keyword)
else -> null else -> null
} }
@ -145,4 +147,6 @@ data class SearchUiState(
val searchHistories: ImmutableList<SearchHistory> = persistentListOf(), val searchHistories: ImmutableList<SearchHistory> = persistentListOf(),
) : UiState ) : UiState
sealed interface SearchUiEvent : UiEvent sealed interface SearchUiEvent : UiEvent {
data class KeywordChanged(val keyword: String) : SearchUiEvent
}

View File

@ -30,16 +30,20 @@ import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.SearchForumBean import com.huanchengfly.tieba.post.api.models.SearchForumBean
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.Chip
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@ -53,20 +57,17 @@ fun SearchForumPage(
viewModel.send(SearchForumUiIntent.Refresh(keyword)) viewModel.send(SearchForumUiIntent.Refresh(keyword))
viewModel.initialized = true viewModel.initialized = true
} }
val currentKeyword by viewModel.uiState.collectPartialAsState(
val shouldLoad = LocalShouldLoad.current prop1 = SearchForumUiState::keyword,
LaunchedEffect(keyword) { initial = ""
if (viewModel.initialized) { )
if (shouldLoad) {
viewModel.send(SearchForumUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
val isRefreshing by viewModel.uiState.collectPartialAsState( val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::isRefreshing, prop1 = SearchForumUiState::isRefreshing,
initial = false initial = true
)
val error by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::error,
initial = null
) )
val exactMatchForum by viewModel.uiState.collectPartialAsState( val exactMatchForum by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::exactMatchForum, prop1 = SearchForumUiState::exactMatchForum,
@ -89,6 +90,36 @@ fun SearchForumPage(
onRefresh = { viewModel.send(SearchForumUiIntent.Refresh(keyword)) } onRefresh = { viewModel.send(SearchForumUiIntent.Refresh(keyword)) }
) )
val isEmpty by remember {
derivedStateOf { !showExactMatchResult && !showFuzzyMatchResult }
}
onGlobalEvent<SearchUiEvent.KeywordChanged> {
viewModel.send(SearchForumUiIntent.Refresh(it.keyword))
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(currentKeyword) {
if (currentKeyword.isNotEmpty() && keyword != currentKeyword) {
if (shouldLoad) {
viewModel.send(SearchForumUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
StateScreen(
isEmpty = isEmpty,
isError = error != null,
isLoading = isRefreshing,
onReload = { viewModel.send(SearchForumUiIntent.Refresh(keyword)) },
errorScreen = {
error?.let {
val (e) = it
ErrorScreen(error = e)
}
}
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -153,6 +184,8 @@ fun SearchForumPage(
} }
} }
}
@Composable @Composable
private fun SearchForumItem( private fun SearchForumItem(
item: SearchForumBean.ForumInfoBean, item: SearchForumBean.ForumInfoBean,

View File

@ -93,7 +93,7 @@ data class SearchForumUiState(
val keyword: String = "", val keyword: String = "",
val exactMatchForum: SearchForumBean.ForumInfoBean? = null, val exactMatchForum: SearchForumBean.ForumInfoBean? = null,
val fuzzyMatchForumList: List<SearchForumBean.ForumInfoBean> = persistentListOf(), val fuzzyMatchForumList: List<SearchForumBean.ForumInfoBean> = persistentListOf(),
val isRefreshing: Boolean = false, val isRefreshing: Boolean = true,
val error: ImmutableHolder<Throwable>? = null, val error: ImmutableHolder<Throwable>? = null,
) : UiState ) : UiState

View File

@ -16,7 +16,9 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -30,8 +32,10 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.Card import com.huanchengfly.tieba.post.ui.widgets.compose.Card
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.ForumInfoChip import com.huanchengfly.tieba.post.ui.widgets.compose.ForumInfoChip
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
@ -42,6 +46,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadContent
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadReplyBtn import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadReplyBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadShareBtn import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadShareBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -60,9 +65,13 @@ fun SearchThreadPage(
viewModel.send(SearchThreadUiIntent.Refresh(keyword, initialSortType)) viewModel.send(SearchThreadUiIntent.Refresh(keyword, initialSortType))
viewModel.initialized = true viewModel.initialized = true
} }
val currentKeyword by viewModel.uiState.collectPartialAsState(
prop1 = SearchThreadUiState::keyword,
initial = ""
)
val isRefreshing by viewModel.uiState.collectPartialAsState( val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = SearchThreadUiState::isRefreshing, prop1 = SearchThreadUiState::isRefreshing,
initial = false initial = true
) )
val isLoadingMore by viewModel.uiState.collectPartialAsState( val isLoadingMore by viewModel.uiState.collectPartialAsState(
prop1 = SearchThreadUiState::isLoadingMore, prop1 = SearchThreadUiState::isLoadingMore,
@ -88,9 +97,13 @@ fun SearchThreadPage(
prop1 = SearchThreadUiState::sortType, prop1 = SearchThreadUiState::sortType,
initial = initialSortType initial = initialSortType
) )
onGlobalEvent<SearchThreadUiEvent.SwitchSortType> {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, it.sortType))
}
val shouldLoad = LocalShouldLoad.current val shouldLoad = LocalShouldLoad.current
LaunchedEffect(keyword) { LaunchedEffect(currentKeyword) {
if (viewModel.initialized) { if (currentKeyword.isNotEmpty() && keyword != currentKeyword) {
if (shouldLoad) { if (shouldLoad) {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType)) viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType))
} else { } else {
@ -99,16 +112,32 @@ fun SearchThreadPage(
} }
} }
onGlobalEvent<SearchThreadUiEvent.SwitchSortType> {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, it.sortType))
}
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing, refreshing = isRefreshing,
onRefresh = { viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType)) } onRefresh = { viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType)) }
) )
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val isEmpty by remember {
derivedStateOf { data.isEmpty() }
}
onGlobalEvent<SearchUiEvent.KeywordChanged> {
viewModel.send(SearchThreadUiIntent.Refresh(it.keyword, sortType))
}
StateScreen(
isEmpty = isEmpty,
isError = error != null,
isLoading = isRefreshing,
onReload = { viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType)) },
errorScreen = {
error?.let {
val (e) = it
ErrorScreen(error = e)
}
}
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -156,6 +185,7 @@ fun SearchThreadPage(
} }
} }
} }
}
@Composable @Composable
private fun SearchThreadList( private fun SearchThreadList(

View File

@ -136,7 +136,7 @@ sealed interface SearchThreadPartialChange : PartialChange<SearchThreadUiState>
} }
data class SearchThreadUiState( data class SearchThreadUiState(
val isRefreshing: Boolean = false, val isRefreshing: Boolean = true,
val isLoadingMore: Boolean = false, val isLoadingMore: Boolean = false,
val error: ImmutableHolder<Throwable>? = null, val error: ImmutableHolder<Throwable>? = null,
val currentPage: Int = 1, val currentPage: Int = 1,

View File

@ -31,15 +31,19 @@ import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.SearchUserBean import com.huanchengfly.tieba.post.api.models.SearchUserBean
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.Chip
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.utils.StringUtil import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -55,19 +59,17 @@ fun SearchUserPage(
viewModel.send(SearchUserUiIntent.Refresh(keyword)) viewModel.send(SearchUserUiIntent.Refresh(keyword))
viewModel.initialized = true viewModel.initialized = true
} }
val shouldLoad = LocalShouldLoad.current val currentKeyword by viewModel.uiState.collectPartialAsState(
LaunchedEffect(keyword) { prop1 = SearchUserUiState::keyword,
if (viewModel.initialized) { initial = ""
if (shouldLoad) { )
viewModel.send(SearchUserUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
val isRefreshing by viewModel.uiState.collectPartialAsState( val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = SearchUserUiState::isRefreshing, prop1 = SearchUserUiState::isRefreshing,
initial = false initial = true
)
val error by viewModel.uiState.collectPartialAsState(
prop1 = SearchUserUiState::error,
initial = null
) )
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing, refreshing = isRefreshing,
@ -89,6 +91,36 @@ fun SearchUserPage(
derivedStateOf { fuzzyMatch.isNotEmpty() } derivedStateOf { fuzzyMatch.isNotEmpty() }
} }
onGlobalEvent<SearchUiEvent.KeywordChanged> {
viewModel.send(SearchUserUiIntent.Refresh(it.keyword))
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(currentKeyword) {
if (currentKeyword.isNotEmpty() && keyword != currentKeyword) {
if (shouldLoad) {
viewModel.send(SearchUserUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
val isEmpty by remember {
derivedStateOf { !showExactMatchResult && !showFuzzyMatchResult }
}
StateScreen(
isEmpty = isEmpty,
isError = error != null,
isLoading = isRefreshing,
onReload = { viewModel.send(SearchUserUiIntent.Refresh(keyword)) },
errorScreen = {
error?.let {
val (e) = it
ErrorScreen(error = e)
}
}
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -150,6 +182,7 @@ fun SearchUserPage(
) )
} }
} }
}
@Composable @Composable
private fun SearchUserItem( private fun SearchUserItem(

View File

@ -91,7 +91,7 @@ sealed interface SearchUserPartialChange : PartialChange<SearchUserUiState> {
} }
data class SearchUserUiState( data class SearchUserUiState(
val isRefreshing: Boolean = false, val isRefreshing: Boolean = true,
val error: ImmutableHolder<Throwable>? = null, val error: ImmutableHolder<Throwable>? = null,
val keyword: String = "", val keyword: String = "",
val exactMatch: SearchUserBean.UserBean? = null, val exactMatch: SearchUserBean.UserBean? = null,

View File

@ -12,7 +12,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@ -27,8 +29,11 @@ fun LazyLoad(
onLoad: () -> Unit, onLoad: () -> Unit,
) { ) {
val shouldLoad = LocalShouldLoad.current val shouldLoad = LocalShouldLoad.current
LaunchedEffect(loaded, shouldLoad, onLoad) { val curOnLoad by rememberUpdatedState(newValue = onLoad)
if (!loaded && shouldLoad) onLoad() LaunchedEffect(loaded, shouldLoad) {
if (!loaded && shouldLoad) {
curOnLoad()
}
} }
} }