Skip to content

Commit 8f357d9

Browse files
committed
fix: refactor old implementation and move shared logic into one repository used in both home screen and route screen
1 parent 5acb9a3 commit 8f357d9

4 files changed

Lines changed: 117 additions & 80 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.cornellappdev.transit.models.search
2+
3+
import com.cornellappdev.transit.models.Place
4+
import com.cornellappdev.transit.models.RouteRepository
5+
import com.cornellappdev.transit.models.ecosystem.EateryRepository
6+
import com.cornellappdev.transit.models.ecosystem.GymRepository
7+
import com.cornellappdev.transit.networking.ApiResponse
8+
import com.cornellappdev.transit.util.ecosystem.buildEcosystemSearchPlaces
9+
import com.cornellappdev.transit.util.ecosystem.mergeAndRankSearchResults
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.SharingStarted
13+
import kotlinx.coroutines.flow.StateFlow
14+
import kotlinx.coroutines.flow.combine
15+
import kotlinx.coroutines.flow.stateIn
16+
import javax.inject.Inject
17+
import javax.inject.Singleton
18+
19+
/**
20+
* Centralized search data source that merges route search results with ecosystem places.
21+
*
22+
* UI and ViewModels can share this data pipeline to keep search behavior consistent.
23+
*/
24+
@Singleton
25+
class UnifiedSearchRepository @Inject constructor(
26+
private val routeRepository: RouteRepository,
27+
eateryRepository: EateryRepository,
28+
gymRepository: GymRepository,
29+
coroutineScope: CoroutineScope,
30+
) {
31+
32+
val ecosystemSearchPlacesFlow: StateFlow<List<Place>> =
33+
combine(
34+
routeRepository.printerFlow,
35+
routeRepository.libraryFlow,
36+
eateryRepository.eateryFlow,
37+
gymRepository.gymFlow
38+
) { printers, libraries, eateries, gyms ->
39+
buildEcosystemSearchPlaces(
40+
printers = printers,
41+
libraries = libraries,
42+
eateries = eateries,
43+
gyms = gyms
44+
)
45+
}.stateIn(
46+
scope = coroutineScope,
47+
started = SharingStarted.Eagerly,
48+
initialValue = emptyList()
49+
)
50+
51+
fun mergedSearchResults(queryFlow: Flow<String>): Flow<ApiResponse<List<Place>>> =
52+
combine(
53+
queryFlow,
54+
routeRepository.placeFlow,
55+
ecosystemSearchPlacesFlow
56+
) { query, routeSearchResults, ecosystemPlaces ->
57+
mergeAndRankSearchResults(
58+
query = query,
59+
routeSearchResults = routeSearchResults,
60+
ecosystemPlaces = ecosystemPlaces
61+
)
62+
}
63+
}
64+
65+
66+

app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ fun RouteScreen(
109109
skipHalfExpanded = true,
110110
confirmValueChange = {
111111
keyboardController?.hide()
112+
if (it == ModalBottomSheetValue.Hidden) {
113+
routeViewModel.onQueryChange("")
114+
}
112115
true
113116
}
114117
)
@@ -119,6 +122,9 @@ fun RouteScreen(
119122
skipHalfExpanded = true,
120123
confirmValueChange = {
121124
keyboardController?.hide()
125+
if (it == ModalBottomSheetValue.Hidden) {
126+
routeViewModel.onQueryChange("")
127+
}
122128
true
123129
}
124130
)

app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.cornellappdev.transit.models.RouteRepository
1010
import com.cornellappdev.transit.models.SelectedRouteRepository
1111
import com.cornellappdev.transit.models.ecosystem.StaticPlaces
1212
import com.cornellappdev.transit.models.UserPreferenceRepository
13+
import com.cornellappdev.transit.models.search.UnifiedSearchRepository
1314
import com.cornellappdev.transit.models.ecosystem.Eatery
1415
import com.cornellappdev.transit.models.ecosystem.EateryRepository
1516
import com.cornellappdev.transit.models.ecosystem.GymRepository
@@ -19,8 +20,6 @@ import com.cornellappdev.transit.models.ecosystem.UpliftGym
1920
import com.cornellappdev.transit.networking.ApiResponse
2021
import com.cornellappdev.transit.util.StringUtils.fromMetersToMiles
2122
import com.cornellappdev.transit.util.calculateDistance
22-
import com.cornellappdev.transit.util.ecosystem.buildEcosystemSearchPlaces
23-
import com.cornellappdev.transit.util.ecosystem.mergeAndRankSearchResults
2423
import com.google.android.gms.maps.model.LatLng
2524
import dagger.hilt.android.lifecycle.HiltViewModel
2625
import kotlinx.coroutines.flow.MutableStateFlow
@@ -48,6 +47,7 @@ class HomeViewModel @Inject constructor(
4847
private val locationRepository: LocationRepository,
4948
private val eateryRepository: EateryRepository,
5049
private val gymRepository: GymRepository,
50+
private val unifiedSearchRepository: UnifiedSearchRepository,
5151
private val userPreferenceRepository: UserPreferenceRepository,
5252
private val selectedRouteRepository: SelectedRouteRepository
5353
) : ViewModel() {
@@ -118,40 +118,30 @@ class HomeViewModel @Inject constructor(
118118
)
119119
)
120120

121-
private val ecosystemSearchPlacesFlow: StateFlow<List<Place>> =
122-
combine(
123-
routeRepository.printerFlow,
124-
routeRepository.libraryFlow,
125-
eateryRepository.eateryFlow,
126-
gymRepository.gymFlow
127-
) { printers, libraries, eateries, gyms ->
128-
buildEcosystemSearchPlaces(
129-
printers = printers,
130-
libraries = libraries,
131-
eateries = eateries,
132-
gyms = gyms
133-
)
134-
}.stateIn(
121+
private val homeQueryFlow: StateFlow<String> = searchBarUiState
122+
.filterIsInstance<SearchBarUIState.Query>()
123+
.map { it.queryText }
124+
.distinctUntilChanged()
125+
.stateIn(
135126
scope = viewModelScope,
136127
started = SharingStarted.WhileSubscribed(5_000),
137-
initialValue = emptyList()
128+
initialValue = ""
138129
)
139130

131+
private val mergedHomeSearchResultsFlow: StateFlow<ApiResponse<List<Place>>> =
132+
unifiedSearchRepository.mergedSearchResults(homeQueryFlow)
133+
.stateIn(
134+
scope = viewModelScope,
135+
started = SharingStarted.WhileSubscribed(5_000),
136+
initialValue = ApiResponse.Success(emptyList())
137+
)
138+
140139
private val _showAddFavoritesSheet = MutableStateFlow(false)
141140
val showAddFavoritesSheet: StateFlow<Boolean> = _showAddFavoritesSheet.asStateFlow()
142141

143142
val addSearchResultsFlow: StateFlow<ApiResponse<List<Place>>> =
144-
combine(
145-
_addSearchQuery,
146-
placeQueryFlow,
147-
ecosystemSearchPlacesFlow
148-
) { query, routeSearchResults, ecosystemPlaces ->
149-
mergeAndRankSearchResults(
150-
query = query,
151-
routeSearchResults = routeSearchResults,
152-
ecosystemPlaces = ecosystemPlaces
153-
)
154-
}.stateIn(
143+
unifiedSearchRepository.mergedSearchResults(_addSearchQuery)
144+
.stateIn(
155145
scope = viewModelScope,
156146
started = SharingStarted.WhileSubscribed(5_000),
157147
initialValue = ApiResponse.Success(emptyList())
@@ -263,19 +253,8 @@ class HomeViewModel @Inject constructor(
263253
}
264254
}.launchIn(viewModelScope)
265255

266-
combine(
267-
searchBarUiState
268-
.filterIsInstance<SearchBarUIState.Query>()
269-
.map { it.queryText }
270-
.distinctUntilChanged(),
271-
placeQueryFlow,
272-
ecosystemSearchPlacesFlow
273-
) { query, routeSearchResults, ecosystemPlaces ->
274-
query to mergeAndRankSearchResults(
275-
query = query,
276-
routeSearchResults = routeSearchResults,
277-
ecosystemPlaces = ecosystemPlaces
278-
)
256+
combine(homeQueryFlow, mergedHomeSearchResultsFlow) { query, mergedResults ->
257+
query to mergedResults
279258
}.onEach { (query, mergedResults) ->
280259
val currentState = _searchBarUiState.value
281260
if (currentState is SearchBarUIState.Query && currentState.queryText == query) {

app/src/main/java/com/cornellappdev/transit/ui/viewmodels/RouteViewModel.kt

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@ import com.cornellappdev.transit.models.RouteOptions
1414
import com.cornellappdev.transit.models.RouteRepository
1515
import com.cornellappdev.transit.models.SelectedRouteRepository
1616
import com.cornellappdev.transit.models.UserPreferenceRepository
17-
import com.cornellappdev.transit.models.ecosystem.EateryRepository
18-
import com.cornellappdev.transit.models.ecosystem.GymRepository
17+
import com.cornellappdev.transit.models.search.UnifiedSearchRepository
1918
import com.cornellappdev.transit.networking.ApiResponse
2019
import com.cornellappdev.transit.util.TimeUtils
21-
import com.cornellappdev.transit.util.ecosystem.buildEcosystemSearchPlaces
22-
import com.cornellappdev.transit.util.ecosystem.mergeAndRankSearchResults
2320
import com.google.android.gms.maps.model.LatLng
2421
import com.google.android.gms.maps.model.LatLngBounds
2522
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -31,7 +28,7 @@ import kotlinx.coroutines.flow.asStateFlow
3128
import kotlinx.coroutines.flow.combine
3229
import kotlinx.coroutines.flow.debounce
3330
import kotlinx.coroutines.flow.distinctUntilChanged
34-
import kotlinx.coroutines.flow.filterIsInstance
31+
import kotlinx.coroutines.flow.filter
3532
import kotlinx.coroutines.flow.launchIn
3633
import kotlinx.coroutines.flow.map
3734
import kotlinx.coroutines.flow.onEach
@@ -46,8 +43,7 @@ import javax.inject.Inject
4643
class RouteViewModel @Inject constructor(
4744
private val routeRepository: RouteRepository,
4845
private val locationRepository: LocationRepository,
49-
private val eateryRepository: EateryRepository,
50-
private val gymRepository: GymRepository,
46+
private val unifiedSearchRepository: UnifiedSearchRepository,
5147
private val userPreferenceRepository: UserPreferenceRepository,
5248
private val selectedRouteRepository: SelectedRouteRepository
5349
) : ViewModel() {
@@ -131,25 +127,28 @@ class RouteViewModel @Inject constructor(
131127
MutableStateFlow(SearchBarUIState.RecentAndFavorites(emptySet(), emptyList()))
132128
val searchBarUiState: StateFlow<SearchBarUIState> = _searchBarUiState.asStateFlow()
133129

134-
private val ecosystemSearchPlacesFlow: StateFlow<List<Place>> =
135-
combine(
136-
routeRepository.printerFlow,
137-
routeRepository.libraryFlow,
138-
eateryRepository.eateryFlow,
139-
gymRepository.gymFlow
140-
) { printers, libraries, eateries, gyms ->
141-
buildEcosystemSearchPlaces(
142-
printers = printers,
143-
libraries = libraries,
144-
eateries = eateries,
145-
gyms = gyms
146-
)
147-
}.stateIn(
130+
private val routeQueryFlow: StateFlow<String> = searchBarUiState
131+
.map { state ->
132+
when (state) {
133+
is SearchBarUIState.Query -> state.queryText
134+
is SearchBarUIState.RecentAndFavorites -> ""
135+
}
136+
}
137+
.distinctUntilChanged()
138+
.stateIn(
148139
scope = viewModelScope,
149140
started = SharingStarted.WhileSubscribed(5_000),
150-
initialValue = emptyList()
141+
initialValue = ""
151142
)
152143

144+
private val mergedRouteSearchResultsFlow: StateFlow<ApiResponse<List<Place>>> =
145+
unifiedSearchRepository.mergedSearchResults(routeQueryFlow)
146+
.stateIn(
147+
scope = viewModelScope,
148+
started = SharingStarted.WhileSubscribed(5_000),
149+
initialValue = ApiResponse.Success(emptyList())
150+
)
151+
153152
init {
154153
userPreferenceRepository.favoritesFlow.onEach {
155154
if (_searchBarUiState.value is SearchBarUIState.RecentAndFavorites) {
@@ -169,19 +168,8 @@ class RouteViewModel @Inject constructor(
169168
}
170169
}.launchIn(viewModelScope)
171170

172-
combine(
173-
searchBarUiState
174-
.filterIsInstance<SearchBarUIState.Query>()
175-
.map { it.queryText }
176-
.distinctUntilChanged(),
177-
routeRepository.placeFlow,
178-
ecosystemSearchPlacesFlow
179-
) { query, routeSearchResults, ecosystemPlaces ->
180-
query to mergeAndRankSearchResults(
181-
query = query,
182-
routeSearchResults = routeSearchResults,
183-
ecosystemPlaces = ecosystemPlaces
184-
)
171+
combine(routeQueryFlow, mergedRouteSearchResultsFlow) { query, mergedResults ->
172+
query to mergedResults
185173
}.onEach { (query, mergedResults) ->
186174
val currentState = _searchBarUiState.value
187175
if (currentState is SearchBarUIState.Query && currentState.queryText == query) {
@@ -241,11 +229,9 @@ class RouteViewModel @Inject constructor(
241229

242230
}.launchIn(viewModelScope)
243231

244-
searchBarUiState
232+
routeQueryFlow
245233
.debounce(300L)
246-
.filterIsInstance<SearchBarUIState.Query>()
247-
.map { it.queryText }
248-
.distinctUntilChanged()
234+
.filter { it.isNotBlank() }
249235
.onEach {
250236
routeRepository.makeSearch(it)
251237
}.launchIn(viewModelScope)

0 commit comments

Comments
 (0)