房间实体的数据存储正确但检索为空

Room Entity's data is stored correctly but is retrieved as null

我正在尝试做什么

当我的应用程序启动时,我正在使用一个片段,该片段使用 AutoCompleteTextView 和 Places SDK 在用户进行选择时获取 Place 对象。发生这种情况时,我会通过调用 repository.storeWeatherLocation(context,placeId) 通过我的存储库 class 将选定的地点(作为 WeatherLocation 实体)存储在我的房间数据库中,然后在需要时再次获取天气详细信息。

发生了什么事

suspend fun storeWeatherLocationAsync 正在调用 fetchCurrentWeather() & fetchWeeklyWeather() 因为从我能够记录的内容来看,previousLocation 变量为空,尽管数据库检查器显示较旧天气位置数据已经存在。

崩溃详情

我的应用程序崩溃,指出我的 LocationProvider 的 getCustomLocationLat() 正在返回空值(发生在 fetchCurrentWeather() 中)。问题是用户选择的位置已成功存储在我的 Room 数据库中(使用 Database Inspector 检查),那么此函数如何返回 null?

更新:

在使用调试器和 logcat 进行了更多测试后,我发现当应用 运行 时,WeatherLocation 数据被保存在 Room 中。一旦它崩溃并且我重新打开它,该数据再次为空。我在这里错过了什么?我是否以某种方式删除了以前的数据?我实际上没有在 Room 中正确缓存它吗?

数据库class:

@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:

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

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

前言

如果没有完整的代码和完全理解这段代码的含义,就很难具体说明您的问题。如果我的以下一般建议(基于我的猜测和预测)对您没有用,我建议您向存储库添加 link 或简化您的用例以便有人可以帮助您。但同样 - 您在最小可重现示例中包含的代码越多,您得不到具体答案的机会就越大。

我对问题根源的猜测

我的猜测(考虑到你描述的事实)你的麻烦的主要嫌疑人是你的代码部分的覆盖,它们是异步的(例如, 是关于 LiveData 的问题。但同样可以在不同的协程中调用挂起函数等等)。那么我讲的问题的条件是什么?接下来是——将数据保存在本地数据库中,然后读取数据,这两个操作都是异步的,并且在第一个事件和第二个事件之间有一点时间。我真的不明白你的情况是否存在所描述的情况。如果不是,那我就猜错了:-)

我的建议

尝试检查所描述的行为是否真的导致了您的问题。有很多方法可以做到这一点。其中之一 - 改变大小写,当第二个操作(从本地数据库读取)将跟随第一个操作(写入它)时。为此,您可以将第二个操作放在协程中,并在延迟之前添加(我认为 delay(1000) 就足够了)。据我了解,您的函数 - getCustomLocationLat()、getCustomLocationLong() - 是第一个使用此技巧的候选者(可能还有其他函数,但您更容易了解它们)。如果在此测试用例之后,您的问题得到解决 - 您可以考虑可以进行哪些适当的更改以保证第二个事件将始终在第一个事件之后(它可能取决于某些问题的答案 - 1)您能否将两者都放在一起一个协程中的事件? 2) 你能用 LiveData 的观察或延迟替换 LiveData 的解包值吗?)

在添加 Observer 并收到第一个 Observer 之前,您不应该期望从 getValue()LiveData 到 return 的任何房间,除了 null回调中的值。 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
    }

您应该使用具有更直接 return 类型的 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?

在检索到结果之前 return。