Skip to content

Commit 36566a1

Browse files
committed
feat: add search screen
1 parent a45c1c5 commit 36566a1

File tree

4 files changed

+274
-5
lines changed

4 files changed

+274
-5
lines changed

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/search/Navigation.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@ import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptionsBuilder
66
import androidx.navigation.compose.composable
77

8-
const val SEARCH_ROUTE = "search_route"
8+
const val SEARCH_ROUTE = "search"
99

1010
fun NavController.navigateToSearch(navOptions: NavOptionsBuilder.() -> Unit) =
1111
navigate(SEARCH_ROUTE, navOptions)
1212

1313
fun NavGraphBuilder.searchScreen(
14-
14+
// TODO(thatfiredev): handle clicks
1515
) {
1616
composable(route = SEARCH_ROUTE) {
17-
// TODO: Call composable
17+
SearchScreen()
1818
}
1919
}
20-
21-
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package com.google.firebase.example.dataconnect.feature.search
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.Spacer
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.height
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.lazy.LazyRow
9+
import androidx.compose.foundation.lazy.items
10+
import androidx.compose.material3.Button
11+
import androidx.compose.material3.Card
12+
import androidx.compose.material3.ExperimentalMaterial3Api
13+
import androidx.compose.material3.FilterChip
14+
import androidx.compose.material3.ModalBottomSheet
15+
import androidx.compose.material3.OutlinedButton
16+
import androidx.compose.material3.OutlinedTextField
17+
import androidx.compose.material3.RangeSlider
18+
import androidx.compose.material3.SuggestionChip
19+
import androidx.compose.material3.Text
20+
import androidx.compose.material3.TextField
21+
import androidx.compose.material3.rememberModalBottomSheetState
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.runtime.collectAsState
24+
import androidx.compose.runtime.getValue
25+
import androidx.compose.runtime.mutableFloatStateOf
26+
import androidx.compose.runtime.mutableIntStateOf
27+
import androidx.compose.runtime.mutableStateOf
28+
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.rememberCoroutineScope
30+
import androidx.compose.runtime.setValue
31+
import androidx.compose.ui.Alignment
32+
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
33+
import androidx.compose.ui.Modifier
34+
import androidx.compose.ui.text.style.TextAlign
35+
import androidx.compose.ui.unit.dp
36+
import androidx.lifecycle.viewmodel.compose.viewModel
37+
import com.google.firebase.example.dataconnect.feature.profile.ReviewsList
38+
import com.google.firebase.example.dataconnect.ui.components.Actor
39+
import com.google.firebase.example.dataconnect.ui.components.ActorsList
40+
import com.google.firebase.example.dataconnect.ui.components.ErrorCard
41+
import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
42+
import com.google.firebase.example.dataconnect.ui.components.Movie
43+
import com.google.firebase.example.dataconnect.ui.components.MoviesList
44+
import kotlinx.coroutines.launch
45+
46+
47+
@OptIn(ExperimentalMaterial3Api::class) // For BottomSheet and FilterChip
48+
@Composable
49+
fun SearchScreen(
50+
searchViewModel: SearchViewModel = viewModel()
51+
) {
52+
val sheetState = rememberModalBottomSheetState()
53+
val scope = rememberCoroutineScope()
54+
var showBottomSheet by remember { mutableStateOf(true) }
55+
val uiState by searchViewModel.uiState.collectAsState()
56+
57+
when (uiState) {
58+
is SearchUIState.Error -> ErrorCard((uiState as SearchUIState.Error).errorMessage)
59+
SearchUIState.Loading -> LoadingScreen()
60+
is SearchUIState.Success -> {
61+
val searchResult = (uiState as SearchUIState.Success).searchResult
62+
Column {
63+
OutlinedButton(
64+
onClick = {
65+
showBottomSheet = true
66+
},
67+
modifier = Modifier.align(Alignment.CenterHorizontally)
68+
.padding(8.dp)
69+
) {
70+
Text("Try a different query")
71+
}
72+
MoviesList(
73+
listTitle = "Movie Results",
74+
movies = searchResult.moviesMatchingTitle.mapNotNull {
75+
Movie(
76+
it.id.toString(),
77+
it.imageUrl,
78+
it.title,
79+
it.rating?.toFloat()
80+
)
81+
},
82+
onMovieClicked = {
83+
// TODO(thatfiredev): Navigate to movie detail screen
84+
}
85+
)
86+
ActorsList(
87+
listTitle = "Actor Results",
88+
actors = searchResult.actorsMatchingName.mapNotNull {
89+
Actor(
90+
it.id.toString(),
91+
it.imageUrl,
92+
it.name
93+
)
94+
},
95+
onActorClicked = {
96+
// TODO(thatfiredev): Navigate to actor detail screen
97+
}
98+
)
99+
// TODO: Add reviews list
100+
}
101+
}
102+
}
103+
104+
if (showBottomSheet) {
105+
ModalBottomSheet(
106+
onDismissRequest = {
107+
showBottomSheet = false
108+
},
109+
sheetState = sheetState
110+
) {
111+
var query by remember { mutableStateOf("") }
112+
var minYear by remember { mutableIntStateOf(2000) }
113+
var maxYear by remember { mutableIntStateOf(2024) }
114+
var minRating by remember { mutableFloatStateOf(1f) }
115+
var maxRating by remember { mutableFloatStateOf(5f) }
116+
var selectedGenre by remember { mutableStateOf("Action") }
117+
val genres = listOf("Action", "Crime", "Drama", "Sci-Fi")
118+
119+
Column(modifier = Modifier.padding(16.dp)) {
120+
OutlinedTextField(
121+
value = query,
122+
onValueChange = {
123+
query = it
124+
},
125+
label = { Text("Search") },
126+
modifier = Modifier.fillMaxWidth()
127+
)
128+
129+
Spacer(modifier = Modifier.height(16.dp))
130+
131+
RangeSlider(
132+
value = minYear.toFloat()..maxYear.toFloat(),
133+
onValueChange = {
134+
minYear = it.start.toInt()
135+
maxYear = it.endInclusive.toInt()
136+
if (minYear == maxYear) {
137+
if (maxYear < 2024) {
138+
maxYear++
139+
} else if (minYear > 1900) {
140+
minYear--
141+
}
142+
}
143+
},
144+
valueRange = 2000f..2024f,
145+
steps = 24
146+
)
147+
Text(
148+
text = "Year: $minYear - $maxYear",
149+
textAlign = TextAlign.Center,
150+
modifier = Modifier.fillMaxWidth()
151+
)
152+
153+
Spacer(modifier = Modifier.height(16.dp))
154+
155+
RangeSlider(
156+
value = minRating.toFloat()..maxRating.toFloat(),
157+
onValueChange = {
158+
// Round the values to the nearest 0.5
159+
minRating = (Math.round(it.start * 2) / 2.0).toFloat()
160+
maxRating = (Math.round(it.endInclusive * 2) / 2.0).toFloat()
161+
if (minRating == maxRating) {
162+
if (maxRating < 5f) {
163+
maxRating += 0.5f
164+
} else if (minRating > 1f) {
165+
minRating -= 0.5f
166+
}
167+
}
168+
},
169+
valueRange = 1f..5f,
170+
steps = 9
171+
)
172+
Text(
173+
"Rating: $minRating - $maxRating",
174+
textAlign = TextAlign.Center,
175+
modifier = Modifier.fillMaxWidth()
176+
)
177+
178+
Spacer(modifier = Modifier.height(16.dp))
179+
180+
LazyRow {
181+
items(genres) { genre ->
182+
FilterChip(
183+
onClick = { selectedGenre = genre },
184+
label = { Text(genre) },
185+
selected = selectedGenre == genre,
186+
modifier = Modifier.padding(4.dp)
187+
)
188+
}
189+
}
190+
Button(
191+
onClick = {
192+
searchViewModel.search(query, minYear, maxYear,
193+
minRating.toDouble(), maxRating.toDouble(), selectedGenre)
194+
scope.launch { sheetState.hide() }.invokeOnCompletion {
195+
if (!sheetState.isVisible) {
196+
showBottomSheet = false
197+
}
198+
}
199+
},
200+
modifier = Modifier.align(Alignment.CenterHorizontally)
201+
.fillMaxWidth(0.6f)
202+
) {
203+
Text("Search")
204+
}
205+
}
206+
207+
}
208+
}
209+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.google.firebase.example.dataconnect.feature.search
2+
3+
import com.google.firebase.dataconnect.movies.FuzzySearchQuery
4+
import com.google.firebase.dataconnect.movies.MoviesRecentlyReleasedQuery
5+
import com.google.firebase.dataconnect.movies.MoviesTop10Query
6+
7+
sealed class SearchUIState {
8+
9+
data object Loading: SearchUIState()
10+
11+
data class Error(val errorMessage: String): SearchUIState()
12+
13+
data class Success(
14+
val searchResult: FuzzySearchQuery.Data
15+
) : SearchUIState()
16+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.google.firebase.example.dataconnect.feature.search
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import com.google.firebase.dataconnect.movies.MoviesConnector
6+
import com.google.firebase.dataconnect.movies.execute
7+
import com.google.firebase.dataconnect.movies.instance
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.flow.StateFlow
10+
import kotlinx.coroutines.launch
11+
12+
class SearchViewModel(
13+
private val moviesConnector: MoviesConnector = MoviesConnector.instance
14+
) : ViewModel() {
15+
16+
private val _uiState = MutableStateFlow<SearchUIState>(SearchUIState.Loading)
17+
val uiState: StateFlow<SearchUIState>
18+
get() = _uiState
19+
20+
fun search(
21+
userQuery: String,
22+
minYear: Int,
23+
maxYear: Int,
24+
minRating: Double,
25+
maxRating: Double,
26+
genre: String
27+
) {
28+
viewModelScope.launch {
29+
try {
30+
val result = moviesConnector.fuzzySearch.execute(
31+
minYear = minYear,
32+
maxYear = maxYear,
33+
minRating = minRating,
34+
maxRating = maxRating,
35+
genre = genre,
36+
{
37+
input = userQuery
38+
}
39+
)
40+
_uiState.value = SearchUIState.Success(result.data)
41+
} catch (e: Exception) {
42+
_uiState.value = SearchUIState.Error(e.message ?: "Unknown error")
43+
}
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)