Skip to content

Commit

Permalink
Merge pull request #574 from chigichan24/implement-search-feature
Browse files Browse the repository at this point in the history
Implement feature that displays results according to search input text
  • Loading branch information
takahirom authored Aug 17, 2023
2 parents 2e5c902 + 24db273 commit ee37fa1
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ private fun KaigiNavHost(
onNavigationIconClick = {
navController.popBackStack()
},
onTimetableItemClick = { timetableItem ->
navController.navigateToTimetableItemDetailScreen(
timetableItem.id,
)
},
)
sponsorsScreen(
onNavigationIconClick = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.github.droidkaigi.confsched2023.sessions

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
Expand All @@ -17,18 +19,43 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import io.github.droidkaigi.confsched2023.model.DroidKaigi2023Day
import io.github.droidkaigi.confsched2023.model.TimetableCategory
import io.github.droidkaigi.confsched2023.model.TimetableItem
import io.github.droidkaigi.confsched2023.sessions.SearchScreenUiState.Empty
import io.github.droidkaigi.confsched2023.sessions.SearchScreenUiState.SearchList
import io.github.droidkaigi.confsched2023.sessions.component.EmptySearchResultBody
import io.github.droidkaigi.confsched2023.sessions.component.SearchFilter
import io.github.droidkaigi.confsched2023.sessions.component.SearchFilterUiState
import io.github.droidkaigi.confsched2023.sessions.component.SearchTextFieldAppBar
import io.github.droidkaigi.confsched2023.sessions.section.SearchList
import io.github.droidkaigi.confsched2023.sessions.section.SearchListUiState

const val searchScreenRoute = "search"
const val SearchScreenTestTag = "SearchScreen"

fun NavGraphBuilder.searchScreen(onNavigationIconClick: () -> Unit) {
sealed interface SearchScreenUiState {
val searchQuery: String
val searchFilterUiState: SearchFilterUiState

data class Empty(
override val searchQuery: String,
override val searchFilterUiState: SearchFilterUiState,
) : SearchScreenUiState

data class SearchList(
override val searchQuery: String,
override val searchFilterUiState: SearchFilterUiState,
val searchListUiState: SearchListUiState,
) : SearchScreenUiState
}

fun NavGraphBuilder.searchScreen(
onNavigationIconClick: () -> Unit,
onTimetableItemClick: (TimetableItem) -> Unit,
) {
composable(searchScreenRoute) {
SearchScreen(
onBackClick = onNavigationIconClick,
onTimetableItemClick = onTimetableItemClick,
)
}
}
Expand All @@ -40,63 +67,73 @@ fun NavController.navigateSearchScreen() {
@Composable
fun SearchScreen(
onBackClick: () -> Unit,
onTimetableItemClick: (TimetableItem) -> Unit,
modifier: Modifier = Modifier,
viewModel: SearchScreenViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()

SearchScreen(
uiState = uiState,
modifier = modifier,
onBackClick = onBackClick,
searchQuery = uiState.searchQuery,
onSearchQueryChanged = viewModel::onSearchQueryChanged,
searchFilterUiState = uiState.searchFilterUiState,
onDaySelected = viewModel::onDaySelected,
onFilterCategoryChipClicked = viewModel::onFilterCategoryChipClicked,
onCategoriesSelected = viewModel::onCategoriesSelected,
onTimetableItemClick = onTimetableItemClick,
onBookmarkClick = viewModel::onClickTimetableItemBookmark,
)
}

data class SearchScreenUiState(
val searchQuery: String,
val searchFilterUiState: SearchFilterUiState,
)

@Composable
private fun SearchScreen(
searchFilterUiState: SearchFilterUiState,
uiState: SearchScreenUiState,
modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
searchQuery: String = "",
onSearchQueryChanged: (String) -> Unit = {},
onDaySelected: (DroidKaigi2023Day, Boolean) -> Unit = { _, _ -> },
onFilterCategoryChipClicked: () -> Unit = {},
onCategoriesSelected: (TimetableCategory, Boolean) -> Unit = { _, _ -> },
onTimetableItemClick: (TimetableItem) -> Unit = {},
onBookmarkClick: (TimetableItem) -> Unit = {},
) {
val scrollState = rememberLazyListState()
Scaffold(
modifier = modifier.testTag(SearchScreenTestTag),
topBar = {
SearchTextFieldAppBar(
searchQuery = searchQuery,
searchQuery = uiState.searchQuery,
onSearchQueryChanged = onSearchQueryChanged,
onBackClick = onBackClick,
)
},
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding),
modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),
) {
Divider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.outline,
)
SearchFilter(
searchFilterUiState = searchFilterUiState,
searchFilterUiState = uiState.searchFilterUiState,
onDaySelected = onDaySelected,
onFilterCategoryChipClicked = onFilterCategoryChipClicked,
onCategoriesSelected = onCategoriesSelected,
)
EmptySearchResultBody()
when (uiState) {
is Empty -> EmptySearchResultBody(missedQuery = uiState.searchQuery)
is SearchList -> SearchList(
contentPaddingValues = PaddingValues(
bottom = innerPadding.calculateBottomPadding(),
),
scrollState = scrollState,
searchListUiState = uiState.searchListUiState,
onTimetableItemClick = onTimetableItemClick,
onBookmarkIconClick = onBookmarkClick,
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.droidkaigi.confsched2023.designsystem.strings.AppStrings
import io.github.droidkaigi.confsched2023.model.DroidKaigi2023Day
import io.github.droidkaigi.confsched2023.model.Filters
import io.github.droidkaigi.confsched2023.model.SessionsRepository
import io.github.droidkaigi.confsched2023.model.Timetable
import io.github.droidkaigi.confsched2023.model.TimetableCategory
import io.github.droidkaigi.confsched2023.model.TimetableItem
import io.github.droidkaigi.confsched2023.sessions.component.SearchFilterUiState
import io.github.droidkaigi.confsched2023.sessions.section.SearchListUiState
import io.github.droidkaigi.confsched2023.ui.UserMessageStateHolder
import io.github.droidkaigi.confsched2023.ui.buildUiState
import io.github.droidkaigi.confsched2023.ui.handleErrorAndRetry
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -18,21 +28,54 @@ const val SEARCH_QUERY = "searchQuery"
@HiltViewModel
class SearchScreenViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val sessionsRepository: SessionsRepository,
userMessageStateHolder: UserMessageStateHolder,
) : ViewModel() {
private val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "")

private val searchFilterUiState: MutableStateFlow<SearchFilterUiState> = MutableStateFlow(
SearchFilterUiState(),
)

private val sessionsStateFlow: StateFlow<Timetable> = sessionsRepository
.getTimetableStream()
.handleErrorAndRetry(
AppStrings.Retry,
userMessageStateHolder,
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Timetable(),
)

val uiState: StateFlow<SearchScreenUiState> = buildUiState(
searchQuery,
searchFilterUiState,
) { searchQuery, searchFilterUiState ->
SearchScreenUiState(
searchQuery = searchQuery,
searchFilterUiState = searchFilterUiState,
sessionsStateFlow,
) { searchQuery, searchFilterUiState, sessions ->
val searchedSessions = sessions.filtered(
Filters(
days = searchFilterUiState.selectedDays,
categories = searchFilterUiState.selectedCategories,
searchWord = searchQuery,
),
)
if (searchedSessions.isEmpty()) {
SearchScreenUiState.Empty(
searchQuery = searchQuery,
searchFilterUiState = searchFilterUiState,
)
} else {
SearchScreenUiState.SearchList(
searchQuery = searchQuery,
searchFilterUiState = searchFilterUiState,
searchListUiState = SearchListUiState(
bookmarkedTimetableItemIds = sessions.bookmarks,
timetableItems = searchedSessions.timetableItems,
),
)
}
}

fun onSearchQueryChanged(searchQuery: String) {
Expand Down Expand Up @@ -78,4 +121,10 @@ class SearchScreenViewModel @Inject constructor(
},
)
}

fun onClickTimetableItemBookmark(timetableItem: TimetableItem) {
viewModelScope.launch {
sessionsRepository.toggleBookmark(timetableItem.id)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ sealed class SessionsStrings : Strings<SessionsStrings>(Bindings) {
data object UserIcon : SessionsStrings()
data object EventDay : SessionsStrings()
data object Category : SessionsStrings()
class SearchResultNotFound(val missedWord: String) : SessionsStrings()
data object SearchPlaceHolder : SessionsStrings()
data object SearchResultNotFound : SessionsStrings()
data object Bookmark : SessionsStrings()
data object BookmarkFilterAllChip : SessionsStrings()
data object BookmarkedItemNotFound : SessionsStrings()
Expand Down Expand Up @@ -45,8 +45,8 @@ sealed class SessionsStrings : Strings<SessionsStrings>(Bindings) {
Category -> "カテゴリー"
Bookmark -> "Bookmark"
BookmarkFilterAllChip -> "全て"
is SearchResultNotFound -> "${item.missedWord}」と一致する検索結果がありません"
SearchPlaceHolder -> "気になる技術を入力"
SearchResultNotFound -> "この検索条件に一致する結果はありません"
BookmarkedItemNotFound -> "登録されたセッションがありません"
BookmarkedItemNotFoundSideNote -> "気になるセッションをブックマークに追加して\n集めてみましょう!"
Share -> "共有"
Expand Down Expand Up @@ -74,8 +74,8 @@ sealed class SessionsStrings : Strings<SessionsStrings>(Bindings) {
UserIcon -> "User icon"
EventDay -> "Day"
Category -> "Category"
is SearchResultNotFound -> "Nothing matched your search criteria \"${item.missedWord}\""
SearchPlaceHolder -> "Enter some technology"
SearchResultNotFound -> "Nothing matched your search criteria"
Bookmark -> "Bookmark"
BookmarkFilterAllChip -> "All"
BookmarkedItemNotFound -> "No sessions registered"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
Expand All @@ -20,10 +21,11 @@ import io.github.droidkaigi.confsched2023.sessions.SessionsStrings

@Composable
fun EmptySearchResultBody(
missedQuery: String,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
modifier = modifier.fillMaxSize().imePadding(),
contentAlignment = Alignment.Center,
) {
Column(
Expand All @@ -37,7 +39,7 @@ fun EmptySearchResultBody(
)
Spacer(modifier = Modifier.height(28.dp))
Text(
text = SessionsStrings.SearchResultNotFound.asString(),
text = SessionsStrings.SearchResultNotFound(missedQuery).asString(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ private fun SearchTextField(
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
placeholder = {
Text(
text = SearchPlaceHolder.asString(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
if (searchQuery.isBlank()) {
Text(
text = SearchPlaceHolder.asString(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}
},
trailingIcon = {
if (searchQuery.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
Expand Down Expand Up @@ -43,6 +45,7 @@ import io.github.droidkaigi.confsched2023.ui.rememberAsyncImagePainter
const val TimetableListItemTestTag = "TimetableListItem"
const val TimetableListItemBookmarkIconTestTag = "TimetableListItemBookmarkIconTestTag"

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun TimetableListItem(
timetableItem: TimetableItem,
Expand All @@ -58,7 +61,7 @@ fun TimetableListItem(
.clickable { onClick(timetableItem) },
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(modifier = Modifier.weight(1F)) {
FlowRow(modifier = Modifier.weight(1F)) {
chipContent()
}
IconToggleButton(
Expand Down
Loading

0 comments on commit ee37fa1

Please sign in to comment.