fix(city): persist full city list and re-emit dashboard context on re-selection
- Map cityList rows without coordinates; fall back en/ru/be for display names - Replace local city cache on sync when opening city screen - Use SharedFlow + prefs reads so re-selecting the same city (e.g. Minsk) notifies dashboard Made-with: Cursor
This commit is contained in:
@@ -51,6 +51,7 @@ class CityViewModel @Inject constructor(
|
||||
private fun loadCities() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val localeLanguage = Locale.getDefault().language
|
||||
cityService.syncCitiesFromRemote(localeLanguage)
|
||||
val regions = cityService.getCitiesGroupedByCountry(localeLanguage)
|
||||
val selectedCity = cityService.getSelectedCity()
|
||||
val hasOnlyDefaultCity = regions.isEmpty()
|
||||
|
||||
@@ -93,4 +93,10 @@ interface CityService {
|
||||
* @param localeLanguage Device locale language for country display names (e.g. "en", "ru")
|
||||
*/
|
||||
suspend fun getCitiesGroupedByCountry(localeLanguage: String): List<Region>
|
||||
|
||||
/**
|
||||
* Fetches the full city list from the API and replaces the local cache when non-empty.
|
||||
* Safe to call when opening the city picker; on failure, existing cached cities remain.
|
||||
*/
|
||||
suspend fun syncCitiesFromRemote(langId: String?): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import android.content.Context
|
||||
import android.location.Location
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.db3.airmq.sdk.city.data.local.CityLocalDataSource
|
||||
import org.db3.airmq.sdk.city.data.remote.CityRemoteDataSource
|
||||
@@ -30,15 +29,27 @@ class CityServiceImpl @Inject constructor(
|
||||
) : CityService {
|
||||
|
||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val _cityContextFlow = MutableStateFlow(readDashboardContextFromPrefs())
|
||||
|
||||
override fun observeSelectedCity(): Flow<String> = _cityContextFlow.map { it.displayName }
|
||||
/**
|
||||
* Emits after every prefs write. Used instead of [MutableStateFlow] so we still notify
|
||||
* observers when the user re-selects the same city (e.g. Minsk) — equal [DashboardCityContext]
|
||||
* values would otherwise suppress [MutableStateFlow] emissions and leave the dashboard stale.
|
||||
*/
|
||||
private val _cityContextUpdates = MutableSharedFlow<Unit>(replay = 1, extraBufferCapacity = 64)
|
||||
|
||||
override fun observeDashboardCityContext(): Flow<DashboardCityContext> = _cityContextFlow.asStateFlow()
|
||||
init {
|
||||
_cityContextUpdates.tryEmit(Unit)
|
||||
}
|
||||
|
||||
override suspend fun getSelectedCity(): String = _cityContextFlow.value.displayName
|
||||
override fun observeSelectedCity(): Flow<String> =
|
||||
_cityContextUpdates.map { readDashboardContextFromPrefs().displayName }
|
||||
|
||||
override fun getDashboardCityDisplayName(): String = _cityContextFlow.value.displayName
|
||||
override fun observeDashboardCityContext(): Flow<DashboardCityContext> =
|
||||
_cityContextUpdates.map { readDashboardContextFromPrefs() }
|
||||
|
||||
override suspend fun getSelectedCity(): String = readDashboardContextFromPrefs().displayName
|
||||
|
||||
override fun getDashboardCityDisplayName(): String = readDashboardContextFromPrefs().displayName
|
||||
|
||||
override suspend fun initialize(
|
||||
hasLocationPermission: Boolean,
|
||||
@@ -115,7 +126,7 @@ class CityServiceImpl @Inject constructor(
|
||||
|
||||
override suspend fun getResolvedDashboardCityContext(): DashboardCityContext {
|
||||
refreshDashboardCityIdentity()
|
||||
return _cityContextFlow.value
|
||||
return readDashboardContextFromPrefs()
|
||||
}
|
||||
|
||||
override suspend fun getCitiesGroupedByCountry(localeLanguage: String): List<Region> {
|
||||
@@ -132,6 +143,13 @@ class CityServiceImpl @Inject constructor(
|
||||
return grouped
|
||||
}
|
||||
|
||||
override suspend fun syncCitiesFromRemote(langId: String?): Result<Unit> = runCatching {
|
||||
val cities = cityRemoteDataSource.fetchCities(langId).getOrThrow()
|
||||
if (cities.isNotEmpty()) {
|
||||
cityLocalDataSource.replaceAllCities(cities)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readDashboardContextFromPrefs(): DashboardCityContext {
|
||||
val display = prefs.getString(KEY_DASHBOARD_CITY, DEFAULT_CITY_NAME) ?: DEFAULT_CITY_NAME
|
||||
val nameEn = prefs.getString(KEY_DASHBOARD_CITY_EN, DEFAULT_CITY_NAME) ?: DEFAULT_CITY_NAME
|
||||
@@ -140,7 +158,7 @@ class CityServiceImpl @Inject constructor(
|
||||
}
|
||||
|
||||
private fun pushContextUpdate() {
|
||||
_cityContextFlow.value = readDashboardContextFromPrefs()
|
||||
_cityContextUpdates.tryEmit(Unit)
|
||||
}
|
||||
|
||||
private suspend fun ensureCitiesInDb(): List<City> {
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
|
||||
@Dao
|
||||
interface CityDao {
|
||||
@@ -19,4 +20,10 @@ interface CityDao {
|
||||
|
||||
@Query("DELETE FROM city")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Transaction
|
||||
suspend fun replaceAllCities(cities: List<CityEntity>) {
|
||||
deleteAll()
|
||||
insertCities(cities)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ class CityLocalDataSource @Inject constructor(
|
||||
cityDao.insertCities(entities)
|
||||
}
|
||||
|
||||
suspend fun replaceAllCities(cities: List<City>) {
|
||||
val entities = cities.map { it.toEntity() }
|
||||
cityDao.replaceAllCities(entities)
|
||||
}
|
||||
|
||||
private fun CityEntity.toDomain(): City = City(
|
||||
id = id,
|
||||
countryCode = countryCode,
|
||||
|
||||
@@ -43,17 +43,19 @@ class CityRemoteDataSourceImpl @Inject constructor(
|
||||
|
||||
private fun CityListQuery.CityList.toDomain(): City? {
|
||||
val id = _id ?: return null
|
||||
val lat = latitude ?: return null
|
||||
val lon = longitude ?: return null
|
||||
val nameEn = cityName?.en ?: return null
|
||||
val nameEn = cityName?.primaryName() ?: return null
|
||||
return City(
|
||||
id = id,
|
||||
countryCode = countryCode,
|
||||
nameEn = nameEn,
|
||||
nameBe = cityName?.be ?: nameEn,
|
||||
nameRu = cityName?.ru ?: nameEn,
|
||||
latitude = lat.toDouble(),
|
||||
longitude = lon.toDouble(),
|
||||
nameBe = cityName?.be?.takeIf { it.isNotBlank() } ?: nameEn,
|
||||
nameRu = cityName?.ru?.takeIf { it.isNotBlank() } ?: nameEn,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
locationCount = locationCount
|
||||
)
|
||||
}
|
||||
|
||||
/** First non-blank of en → ru → be so we keep cities that omit English in the payload. */
|
||||
private fun CityListQuery.CityName.primaryName(): String? =
|
||||
listOfNotNull(en, ru, be).firstOrNull { it.isNotBlank() }?.trim()
|
||||
|
||||
Reference in New Issue
Block a user