Данные Room Entity хранятся правильно, но возвращаются как нулевые - PullRequest
2 голосов
/ 23 апреля 2020

Что я пытаюсь сделать

Когда мое приложение запускается, я использую фрагмент, который использует AutoCompleteTextView и Places SDK для получения Place объекта когда пользователь делает выбор. Когда это происходит, я сохраняю выбранный объект Place (как объект WeatherLocation) через мой класс Repository в моей базе данных Room, вызывая repository.storeWeatherLocation(context,placeId), а затем при необходимости извлекаю сведения о погоде.

Что происходит

suspend fun storeWeatherLocationAsync вызывает fetchCurrentWeather() & fetchWeeklyWeather(), потому что из того, что я смог записать, переменная previousLocation равна нулю, несмотря на то, что инспектор базы данных показывает, что старые данные о погоде уже присутствует.

Cra sh детали

Мое приложение аварийно завершает работу, сообщая, что getCustomLocationLat() моего LocationProvider возвращает ноль (происходит в fetchCurrentWeather()). Дело в том, что выбранное пользователем местоположение успешно сохраняется в моей базе данных Room (проверено с помощью инспектора базы данных), так как эта функция возвращает ноль?

UPDATE :

Проведя дополнительное тестирование с помощью отладчика и logcat, я обнаружил, что данные WeatherLocation сохраняются в комнате при запуске приложения. Как только он падает, и я снова открываю его, эти данные снова становятся нулевыми. Что мне здесь не хватает? Я как-то удаляю предыдущие данные? Разве я на самом деле не кэширую это правильно в комнате?

Класс базы данных:

@Database(
    entities = [CurrentWeatherEntry::class,WeekDayWeatherEntry::class,WeatherLocation::class],
    version = 16
)
abstract class ForecastDatabase : RoomDatabase() {
    abstract fun currentWeatherDao() : CurrentWeatherDao
    abstract fun weekDayWeatherDao() : WeekDayWeatherDao
    abstract fun weatherLocationDao() : WeatherLocationDao

    // Used to make sure that the ForecastDatabase class will be a singleton
    companion object {
        // Volatile == all of the threads will have immediate access to this property
        @Volatile private var instance:ForecastDatabase? = null
        private val LOCK = Any() // dummy object for thread monitoring

        operator fun invoke(context:Context) = instance ?: synchronized(LOCK) {
            // If the instance var hasn't been initialized, call buildDatabase()
            // and assign it the returned object from the function call (it)
            instance ?: buildDatabase(context).also { instance = it }
        }

        /**
         * Creates an instance of the ForecastDatabase class
         * using Room.databaseBuilder().
         */
        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext,
                ForecastDatabase::class.java, "forecast.db")
                //.addMigrations(MIGRATION_2_3) // specify an explicit Migration Technique
                .fallbackToDestructiveMigration()
                .build()
    }
}

Вот класс репозитория:

class ForecastRepositoryImpl(
    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!!)
            }
            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()
        )
    }

    private fun isFetchWeeklyNeeded(): Boolean {
        val todayEpochTime = LocalDate.now().toEpochDay()
        val futureWeekDayCount = weekDayWeatherDao.countFutureWeekDays(todayEpochTime)
        return futureWeekDayCount < WEEKLY_FORECAST_DAYS_COUNT
    }

    private suspend fun fetchWeeklyWeather() {
        weatherNetworkDataSource.fetchWeeklyWeather(
            locationProvider.getPreferredLocationLat(),
            locationProvider.getPreferredLocationLong()
        )
    }

    override fun storeWeatherLocation(context:Context,placeId: String) {
        GlobalScope.launch(Dispatchers.IO) {
            storeWeatherLocationAsync(context,placeId)
        }
    }

    override suspend fun storeWeatherLocationAsync(context: Context,placeId: String) {
        var isFetchNeeded: Boolean // a flag variable

        // Specify the fields to return.
        val placeFields: List<Place.Field> =
            listOf(Place.Field.ID, Place.Field.NAME,Place.Field.LAT_LNG)

        // Construct a request object, passing the place ID and fields array.
        val request = FetchPlaceRequest.newInstance(placeId, placeFields)

        // Create the client
        val placesClient = Places.createClient(context)

        placesClient.fetchPlace(request).addOnSuccessListener { response ->
            // Get the retrieved place object
            val place = response.place
            // Create a new WeatherLocation object using the place details
            val newWeatherLocation = WeatherLocation(place.latLng!!.latitude,
                place.latLng!!.longitude,place.name!!,place.id!!)

            val previousLocation = weatherLocationDao.getWeatherLocation().value
            if(previousLocation == null || ((newWeatherLocation.latitude != previousLocation.latitude) &&
                (newWeatherLocation.longitude != previousLocation.longitude))) {
                isFetchNeeded = true
                // Store the weatherLocation in the database
                persistWeatherLocation(newWeatherLocation)
                // fetch the data
                GlobalScope.launch(Dispatchers.IO) {
                    // fetch the weather data and wait for it to finish
                    withContext(Dispatchers.Default) {
                        if (isFetchNeeded) {
                            // fetch the weather data using the new location
                            fetchCurrentWeather()
                            fetchWeeklyWeather()
                        }
                    }
                }
            }
            Log.d("REPOSITORY","storeWeatherLocationAsync : inside task called")
        }.addOnFailureListener { exception ->
            if (exception is ApiException) {
                // Handle error with given status code.
                Log.e("Repository", "Place not found: ${exception.statusCode}")
            }
        }
    }

    /**
     * Caches the downloaded current weather data to the local
     * database.
     * @param fetchedCurrentWeather The most recently fetched current weather data
     */
    private fun persistFetchedCurrentWeather(fetchedCurrentWeather: CurrentWeatherResponse) {
        fetchedCurrentWeather.currentWeatherEntry.setTimezone(fetchedCurrentWeather.timezone)
        // Using a GlobalScope since a Repository class doesn't have a lifecycle
        GlobalScope.launch(Dispatchers.IO) {
            currentWeatherDao.upsert(fetchedCurrentWeather.currentWeatherEntry)
        }
    }

    /**
     * Caches the selected location data to the local
     * database.
     * @param fetchedLocation The most recently fetched location data
     */
    private fun persistWeatherLocation(fetchedLocation: WeatherLocation) {
        GlobalScope.launch(Dispatchers.IO) {
            weatherLocationDao.upsert(fetchedLocation)
        }
    }

    /**
     * Caches the downloaded weekly weather data to the local
     * database.
     * @param fetchedWeeklyWeather  The most recently fetched weekly weather data
     */
    private fun persistFetchedWeeklyWeather(fetchedWeeklyWeather: WeeklyWeatherResponse) {

        fun deleteOldData() {
            val time = LocalDate.now().toEpochDay()
            weekDayWeatherDao.deleteOldEntries(time)
        }

        GlobalScope.launch(Dispatchers.IO) {
            deleteOldData()
            val weekDayEntriesList = fetchedWeeklyWeather.weeklyWeatherContainer.weekDayEntries
            weekDayWeatherDao.insert(weekDayEntriesList)
        }
    }
}

, а вот Impl LocationProvider:

class LocationProviderImpl(
    private val fusedLocationProviderClient: FusedLocationProviderClient,
    context: Context,
    private val locationDao: WeatherLocationDao
) : PreferenceProvider(context), LocationProvider {
    private val appContext = context.applicationContext

    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

        // 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 fun getLastDeviceLocationAsync(): Deferred<Location?> {
        return if(hasLocationPermission())
            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)
    }

    private fun getCustomLocationLat() : Double {
        val lat:Double? = locationDao.getWeatherLocation().value?.latitude
        if(lat == null) Log.d("LOCATION_PROVIDER","lat is null = $lat")
        return lat!!
    }

    private fun getCustomLocationLong():Double {
        return locationDao.getWeatherLocation().value!!.longitude
    }

    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()
        }
    }

    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()
        }
    }
}

Ответы [ 2 ]

1 голос
/ 29 апреля 2020

Вы не должны ожидать, что Комната LiveData будет возвращать что-либо, кроме null от getValue() до тех пор, пока Observer не будет добавлено и не получит свое первое значение в обратном вызове. LiveData - это, прежде всего, наблюдаемый держатель данных, а созданные Room - ленивые и асинхронные по своей конструкции, так что они не начнут выполнять фоновую работу с базой данных, чтобы сделать значения доступными, пока не присоединится Observer.

В подобных ситуациях из LocationProviderImpl:

    private fun getCustomLocationLat() : Double {
        val lat:Double? = locationDao.getWeatherLocation().value?.latitude
        if(lat == null) Log.d("LOCATION_PROVIDER","lat is null = $lat")
        return lat!!
    }

    private fun getCustomLocationLong():Double {
        return locationDao.getWeatherLocation().value!!.longitude
    }

вместо этого следует использовать метод Dao с более прямым типом возврата для получения значений, например. в вашем Dao вместо чего-то вроде этого:

@Query("<your query here>")
fun getWeatherLocation(): LiveData<LocationEntity>

создайте и используйте один из них:

@Query("<your query here>")
suspend fun getWeatherLocation(): LocationEntity?

@Query("<your query here>")
fun getWeatherLocationSync(): LocationEntity?

, которые не возвращаются, пока не будет получен результат.

0 голосов
/ 26 апреля 2020

Предисловие

Слишком сложно указать c вашей проблемы без полного кода и полного понимания того, для чего предназначен этот код. Если мои следующие общие рекомендации (основанные на моих догадках и предсказаниях) будут бесполезны для вас, я рекомендую вам либо добавить ссылку на ваш репозиторий, либо упростить ваш вариант использования, чтобы кто-то мог вам помочь. Но опять же - чем больше кода вы включите в свой минимальный воспроизводимый пример, тем больше шансов, что вы не получите конкретный c ответ.

Мои предположения об источнике проблемы

Я полагаю (учитывая факты, которые вы описали), что основным подозрением в вашей проблеме является наложение частей вашего кода, которые асинхронны (например, в этом случае касается проблемы с LiveData. Но то же самое может быть с функции приостановки, вызываемые в различных сопрограммах и т. д.). Итак, каковы условия проблемы, о которой я говорю? Они следующие: вы сохраняете свои данные в локальной базе данных, затем читаете их, оба действия асинхронны, и между первым и вторым событием проходит немного времени. Я действительно не понял, присутствуют ли описанные условия в вашем случае. Если это не так, я не угадал: -)

Мои предложения

Попробуйте проверить, действительно ли описанное поведение вызывает вашу проблему. Есть много способов сделать это. Один из них - изменить случай, когда вторая операция (чтение из локальной базы данных) последует за первой (запись в нее). Для этого вы можете поместить свою вторую операцию в сопрограмму и добавить до некоторой задержки (я думаю, задержки (1000) будет достаточно). Как я понял, ваши функции - getCustomLocationLat (), getCustomLocationLong () - являются первыми кандидатами на эту хитрость (возможно, есть и другие функции, но вам будет проще их узнать). Если после этого контрольного примера ваша проблема будет решена - вы можете подумать, какие соответствующие изменения вы могли бы внести, чтобы гарантировать, что второе событие всегда будет после первого (это может зависеть от ответов на некоторые вопросы - 1). события в одной сопрограмме? 2) не могли бы вы заменить значение распаковки из LiveData наблюдением LiveData или отложенным?)

...