Функция внутри сопрограмм вызывается постоянно и никогда не завершается - PullRequest
0 голосов
/ 04 мая 2020

В моем главном фрагменте я получаю и наблюдаю некоторые 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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...