В моем главном фрагменте я получаю и наблюдаю некоторые LiveData (currentWeather
), доступные через его ViewModel. Живые данные извлекаются через модуль Repository, как вы можете видеть ниже, используя пользовательский делегат LazyDeferred
. После выполнения приложения, когда вызывается метод репозитория fetchCurrentWeather
, моя функция checkIsCurrentLocEnabled()
будет вызываться через функции getPreferredLocationLat()
& getPreferredLocationLong()
. Однако, функция checkIsCurrentLocEnabled
, кажется, вызывается без остановки (проверяется через logcat), и она также, кажется, никогда не останавливается (ни один код ниже val currentWeather
в основном фрагменте не запускается). Что мне не хватает? Связано ли это с вложенными сопрограммами? Использование GlobalScope
или это что-то еще?
MainFragment:
private fun bindUIAsync() = async(Dispatchers.Main) {
// fetch current weather
val currentWeather = viewModel.currentWeatherData.await()
}
ViewModel:
val currentWeatherData by lazyDeferred {
forecastRepository.getCurrentWeather()
}
LazyDeferred делегат:
fun<T> lazyDeferred(block: suspend CoroutineScope.() -> T) : Lazy<Deferred<T>> {
return lazy {
GlobalScope.async {
block.invoke(this)
}
}
}
Репозиторий:
class ForecastRepositoryImpl(
private val context: Context,
private val currentWeatherDao: CurrentWeatherDao,
private val weekDayWeatherDao: WeekDayWeatherDao,
private val weatherLocationDao: WeatherLocationDao,
private val locationProvider: LocationProvider,
private val weatherNetworkDataSource: WeatherNetworkDataSource
) : ForecastRepository {
init {
weatherNetworkDataSource.apply {
// Persist downloaded data
downloadedCurrentWeatherData.observeForever { newCurrentWeather: CurrentWeatherResponse? ->
persistFetchedCurrentWeather(newCurrentWeather!!)
storeTimezone(newCurrentWeather.timezone)
}
downloadedWeeklyWeatherData.observeForever { newWeeklyWeather: WeeklyWeatherResponse? ->
persistFetchedWeeklyWeather(newWeeklyWeather!!)
}
}
}
override suspend fun getCurrentWeather(): LiveData<CurrentWeatherEntry> {
return withContext(Dispatchers.IO) {
initWeatherData()
return@withContext currentWeatherDao.getCurrentWeather()
}
}
override suspend fun getWeekDayWeatherList(time: Long): LiveData<out List<WeekDayWeatherEntry>> {
return withContext(Dispatchers.IO) {
initWeatherData()
return@withContext weekDayWeatherDao.getFutureWeather(time)
}
}
override suspend fun getWeatherLocation(): LiveData<WeatherLocation> {
return withContext(Dispatchers.IO) {
return@withContext weatherLocationDao.getWeatherLocation()
}
}
private suspend fun initWeatherData() {
// retrieve the last weather location from room
val lastWeatherLocation = weatherLocationDao.getWeatherLocation().value
if (lastWeatherLocation == null ||
locationProvider.hasLocationChanged(lastWeatherLocation)
) {
fetchCurrentWeather()
fetchWeeklyWeather()
return
}
val lastFetchedTime = currentWeatherDao.getCurrentWeather().value?.zonedDateTime
if (isFetchCurrentNeeded(lastFetchedTime!!))
fetchCurrentWeather()
if (isFetchWeeklyNeeded())
fetchWeeklyWeather()
}
/**
* Checks if the current weather data should be re-fetched.
* @param lastFetchedTime The time at which the current weather data were last fetched
* @return True or false respectively
*/
private fun isFetchCurrentNeeded(lastFetchedTime: ZonedDateTime): Boolean {
val thirtyMinutesAgo = ZonedDateTime.now().minusMinutes(30)
return lastFetchedTime.isBefore(thirtyMinutesAgo)
}
/**
* Fetches the Current Weather data from the WeatherNetworkDataSource.
*/
private suspend fun fetchCurrentWeather() {
weatherNetworkDataSource.fetchCurrentWeather(
locationProvider.getPreferredLocationLat(),
locationProvider.getPreferredLocationLong()
)
}
...
}
LocationProvider:
const val USE_DEVICE_LOCATION_KEY = "USE_DEVICE_LOCATION"
const val LOCATION_TIMEZONE_KEY = "LOCATION_TMZ"
class LocationProviderImpl(
private val fusedLocationProviderClient: FusedLocationProviderClient,
context: Context,
private val locationDao: WeatherLocationDao
) : PreferenceProvider(context), LocationProvider {
private val appContext = context.applicationContext
private val TAG = LocationProviderImpl::class.java.simpleName
override suspend fun hasLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
return try {
hasDeviceLocationChanged(lastWeatherLocation)
} catch (e: LocationPermissionNotGrantedException) {
false
}
}
/**
* Makes the required checks to determine whether the device's location has
* changed or not.
* @param lastWeatherLocation The last known user selected location
* @return true if the device location has changed or false otherwise
*/
private suspend fun hasDeviceLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
if (!isUsingDeviceLocation()) return false // we don't have location permissions or setting's disabled
val currentDeviceLocation = getLastDeviceLocationAsync().await()
?: return false
Log.i(
"Location Provider",
"lat = ${currentDeviceLocation.latitude} | lon = ${currentDeviceLocation.longitude}"
)
// Check if the old and new locations are far away enough that an update is needed
val comparisonThreshold = 0.03
return abs(currentDeviceLocation.latitude - lastWeatherLocation.latitude) > comparisonThreshold
&& abs(currentDeviceLocation.longitude - lastWeatherLocation.longitude) > comparisonThreshold
}
/**
* Checks if the app has the location permission, and if that's the case
* it will fetch the device's last saved location.
* @return The device's last saved location as a Deferred<Location?>
*/
@SuppressLint("MissingPermission")
private suspend fun getLastDeviceLocationAsync(): Deferred<Location?> {
val hasGpsEnabled = preferences.getBoolean(GPS_IS_ON, false)
checkIsCurrentLocEnabled()
return if (hasLocationPermission() && hasGpsEnabled)
fusedLocationProviderClient.lastLocation.asDeferredAsync()
else
throw LocationPermissionNotGrantedException()
}
/**
* Checks if the user has granted the location
* permission.
*/
private fun hasLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(
appContext,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
/**
* Returns the sharedPrefs value for the USE_DEVICE_LOCATION
* preference with a default value of "true".
*/
private fun isUsingDeviceLocation(): Boolean {
return preferences.getBoolean(USE_DEVICE_LOCATION_KEY, false)
}
/**
* @return Returns the saved location's latitude. If the saved location
* is null it returns "Los Angeles"' lat value.
*/
private fun getCustomLocationLat(): Double {
// if there is no stored latitude return LosAngeles' lat value
return locationDao.getWeatherLocationAsync()?.latitude ?: 37.8267
}
/**
* @return Returns the saved location's longitude. If the saved location
* is null it returns "Los Angeles"' longitude value.
*/
private fun getCustomLocationLong(): Double {
// if there is no stored latitude return LosAngeles' longitude value
return locationDao.getWeatherLocationAsync()?.longitude ?: -122.4233
}
/**
* If the user has turned on the current location setting,
* it returns the current location's latitude. Otherwise it returns
* the stored location's latitude.
* @return The preferred latitude value
*/
override suspend fun getPreferredLocationLat(): Double {
if (isUsingDeviceLocation()) {
try {
val deviceLocation = getLastDeviceLocationAsync().await()
?: return getCustomLocationLat()
return deviceLocation.latitude
} catch (e: LocationPermissionNotGrantedException) {
return getCustomLocationLat()
}
} else {
return getCustomLocationLat()
}
}
/**
* If the user has turned on the current location setting,
* it returns the current location's longitude. Otherwise it returns
* the stored location's longitude.
* @return The preferred longitude value
*/
override suspend fun getPreferredLocationLong(): Double {
if (isUsingDeviceLocation()) {
try {
val deviceLocation = getLastDeviceLocationAsync().await()
?: return getCustomLocationLong()
return deviceLocation.longitude
} catch (e: LocationPermissionNotGrantedException) {
return getCustomLocationLong()
}
} else {
return getCustomLocationLong()
}
}
/**
* Stores the selected location's timezone in the app's sharedPrefs
*/
override fun storeTimezone(tmz: String) {
preferences.edit().putString(LOCATION_TIMEZONE_KEY, tmz).commit()
}
/**
* Stores the location in the app's database.
* @param location The location to save
*/
private fun persistWeatherLocation(location:WeatherLocation) {
GlobalScope.launch(Dispatchers.IO) {
locationDao.upsert(location)
}
}
/**
* Checks if the user decided to use the current
* location setting for fetching weather.
* @return True if the user did or false otherwise
*/
override fun isUsingCurrentLocationSetting() : Boolean {
return hasLocationPermission() && isGpsOn()
}
/**
* Checks if the device's GPS is on.
* @return True if it is or false otherwise
*/
private fun isGpsOn(): Boolean {
return preferences.getBoolean(GPS_IS_ON,false)
}
/**
* Retrieves the device's current location locality
* asynchronously.
* @return The locality wrapped in a Deferred<T>
*/
override fun getCurrentLocationLocalityAsync() : Deferred<String> {
return GlobalScope.async(Dispatchers.IO) {
val gcd = Geocoder(appContext, Locale.getDefault())
val addresses = gcd.getFromLocation(getPreferredLocationLat(),
getPreferredLocationLong(), 1)
return@async if(addresses.size >0) addresses[0].locality else "Error"
}
}
/**
* Checks if the current location setting criteria
* are satisfied. If that's the case it stores the device's
* current location in the app's database asynchronously.
*/
override suspend fun checkIsCurrentLocEnabled() {
//TODO:sp error here -> gets called non stop and never finishes
if(isUsingCurrentLocationSetting()) {
withContext(Dispatchers.IO) {
val locality = getCurrentLocationLocalityAsync().await()
persistWeatherLocation(
WeatherLocation(getPreferredLocationLat(),
getPreferredLocationLong(),locality,"current_loc")
)
}
}
}
}
asDeferredAsyn c:
fun <T> Task<T>.asDeferredAsync() : Deferred<T> {
val deferred = CompletableDeferred<T>()
this.addOnSuccessListener { result ->
deferred.complete(result)
}
this.addOnFailureListener { exception ->
deferred.completeExceptionally(exception)
}
return deferred
}