Skip to content

Commit

Permalink
Improve BU selector in search view (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
MGaetan89 authored Nov 3, 2023
1 parent aa2245d commit 09e14c6
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
*/
package ch.srgssr.pillarbox.demo.ui.integrationLayer

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
Expand All @@ -25,8 +31,12 @@ import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
Expand Down Expand Up @@ -54,13 +64,27 @@ fun SearchView(searchViewModel: SearchViewModel, onSearchClicked: (Content.Media
val currentBu by searchViewModel.bu.collectAsState()
val searchQuery by searchViewModel.query.collectAsState()
val focusRequester = remember { FocusRequester() }
val hasNoContent = lazyItems.itemCount == 0 &&
lazyItems.loadState.refresh is LoadState.NotLoading &&
!searchViewModel.hasValidSearchQuery()

Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
var showBuSelector by remember { mutableStateOf(true) }

AnimatedVisibility(visible = showBuSelector) {
BuSelector(
listBu = bus,
selectedBu = currentBu,
onBuSelected = searchViewModel::selectBu
)
}

TextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.focusRequester(focusRequester),
trailingIcon = {
IconButton(onClick = searchViewModel::clear) {
Expand All @@ -70,17 +94,24 @@ fun SearchView(searchViewModel: SearchViewModel, onSearchClicked: (Content.Media
)
}
},
prefix = if (showBuSelector) {
null
} else {
{
Text(text = "[${currentBu.name.uppercase()}] ")
}
},
singleLine = true,
maxLines = 1,
placeholder = { Text(text = "Search") },
value = searchQuery,
onValueChange = searchViewModel::setQuery
)
if (lazyItems.itemCount == 0 && lazyItems.loadState.refresh is LoadState.NotLoading) {
if (hasNoContent) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
.padding(horizontal = 8.dp),
contentAlignment = Alignment.Center
) {
Text(text = "No content")
Expand All @@ -89,8 +120,9 @@ fun SearchView(searchViewModel: SearchViewModel, onSearchClicked: (Content.Media
SearchResultList(
lazyPagingItems = lazyItems,
contentClick = onSearchClicked,
currentBu = currentBu,
buClicked = searchViewModel::selectBu
onScroll = { showBuSelector = it },
searchViewModel = searchViewModel,
modifier = Modifier.padding(horizontal = 8.dp)
)
}

Expand All @@ -101,49 +133,45 @@ fun SearchView(searchViewModel: SearchViewModel, onSearchClicked: (Content.Media

@Composable
private fun SearchResultList(
lazyPagingItems: LazyPagingItems<SearchContent>,
searchViewModel: SearchViewModel,
lazyPagingItems: LazyPagingItems<Content.Media>,
contentClick: (Content.Media) -> Unit,
currentBu: Bu,
buClicked: (Bu) -> Unit,
onScroll: (scrollUp: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val hasNoResult = lazyPagingItems.loadState.refresh is LoadState.NotLoading &&
lazyPagingItems.itemCount == 1 &&
lazyPagingItems[0] is SearchContent.BuSelector
val hasNoResult = lazyPagingItems.itemCount == 0 &&
lazyPagingItems.loadState.refresh is LoadState.NotLoading &&
searchViewModel.hasValidSearchQuery()
val scrollState = rememberLazyListState()
val isScrollingUp = scrollState.isScrollingUp()

LazyColumn(modifier = modifier) {
LaunchedEffect(isScrollingUp) {
onScroll(isScrollingUp)
}

LazyColumn(
modifier = modifier,
state = scrollState
) {
items(count = lazyPagingItems.itemCount, key = lazyPagingItems.itemKey()) { index ->
val item = lazyPagingItems[index]
item?.let { searchContent ->
when (searchContent) {
is SearchContent.MediaResult -> {
ContentView(
content = searchContent.media,
Modifier
.padding(bottom = 2.dp)
.fillMaxWidth()
.minimumInteractiveComponentSize()
.clickable { contentClick(searchContent.media) }
)
}

is SearchContent.BuSelector -> {
BuSelector(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
listBu = bus, selectedBu = currentBu, onBuSelected = buClicked
)
}
}
item?.let { mediaResult ->
ContentView(
content = mediaResult,
modifier = Modifier
.padding(bottom = 2.dp)
.fillMaxWidth()
.minimumInteractiveComponentSize()
.clickable { contentClick(mediaResult) }
)
}
}
if (hasNoResult) {
item {
NoResult(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.padding(horizontal = 8.dp)
)
}
}
Expand All @@ -164,17 +192,42 @@ private fun SearchResultList(
}
}

// Snippet from a Google Codelab:
// https://github.com/android/codelab-android-compose/blob/main/AnimationCodelab/finished/src/main/java/com/example/android/codelab/animation/ui/home/Home.kt#L339
@Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }

return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun BuSelector(listBu: List<Bu>, selectedBu: Bu, onBuSelected: (Bu) -> Unit, modifier: Modifier = Modifier) {
Row(
LazyRow(
modifier = modifier,
contentPadding = PaddingValues(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (bu in listBu) {
Button(onClick = { onBuSelected(bu) }, enabled = bu != selectedBu) {
Text(text = bu.name.uppercase())
}
items(listBu) { bu ->
FilterChip(
selected = bu == selectedBu,
onClick = { onBuSelected(bu) },
label = { Text(text = bu.name.uppercase()) },
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.insertHeaderItem
import androidx.paging.map
import ch.srg.dataProvider.integrationlayer.request.parameters.Bu
import ch.srgssr.pillarbox.demo.ui.integrationLayer.data.Content
import ch.srgssr.pillarbox.demo.ui.integrationLayer.data.ILRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -48,17 +48,17 @@ class SearchViewModel(private val ilRepository: ILRepository) : ViewModel() {
*/
val query: StateFlow<String> = _query

@OptIn(FlowPreview::class)
private val config = combine(bu, query) { bu, query -> Config(bu, query) }.debounce(600.milliseconds)

/**
* Result of the search trigger by [bu] and [query]
*/
@OptIn(ExperimentalCoroutinesApi::class)
val result: Flow<PagingData<SearchContent>> = config.flatMapLatest { config ->
if (config.query.length >= 3) {
ilRepository.search(config.bu, config.query).map { mediaPagingData ->
val pagingData: PagingData<SearchContent> = mediaPagingData.map { item -> SearchContent.MediaResult(Content.Media(item)) }
pagingData.insertHeaderItem(item = SearchContent.BuSelector)
val result: Flow<PagingData<Content.Media>> = config.flatMapLatest { (bu, query) ->
if (hasValidSearchQuery(query)) {
ilRepository.search(bu, query).map { mediaPagingData ->
mediaPagingData.map { item -> Content.Media(item) }
}.cachedIn(viewModelScope)
} else {
val loadState = LoadState.NotLoading(true)
Expand Down Expand Up @@ -99,6 +99,15 @@ class SearchViewModel(private val ilRepository: ILRepository) : ViewModel() {
_bu.value = bu
}

/**
* Checks if the provided [query] is valid.
*
* @return `true` if [query] has at least [VALID_SEARCH_QUERY_THRESHOLD] characters, `false` otherwise
*/
fun hasValidSearchQuery(query: String = this.query.value): Boolean {
return query.length >= VALID_SEARCH_QUERY_THRESHOLD
}

internal data class Config(val bu: Bu, val query: String)

@Suppress("UndocumentedPublicClass")
Expand All @@ -109,21 +118,8 @@ class SearchViewModel(private val ilRepository: ILRepository) : ViewModel() {
return SearchViewModel(ilRepository) as T
}
}
}

/**
* Search content
*/
sealed interface SearchContent {
/**
* Bu selector
*/
object BuSelector : SearchContent

/**
* Search Media result data
*
* @property media
*/
data class MediaResult(val media: Content.Media) : SearchContent
private companion object {
private const val VALID_SEARCH_QUERY_THRESHOLD = 3
}
}

0 comments on commit 09e14c6

Please sign in to comment.