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

Ste*_*ail 8 android kotlin android-room android-livedata

我正在尝试做什么

当我的应用程序启动时,我使用一个片段,该片段使用AutoCompleteTextViewPlaces SDKPlace在用户做出选择时获取对象。发生这种情况时,我通过调用将选定的地点(作为 WeatherLocation 实体)存储在我的 Room 数据库中repository.storeWeatherLocation(context,placeId),然后根据需要再次获取天气详细信息。

发生了什么

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

崩溃详情

我的应用程序崩溃,表明我的 LocationProvidergetCustomLocationLat()返回 null(发生在fetchCurrentWeather())。问题是用户选择的位置已成功存储在我的 Room 数据库中(使用 Database Inspector 检查),那么这个函数如何返回 null?

更新

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

数据库类:

@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()
    }
}
Run Code Online (Sandbox Code Playgroud)

这是存储库类:

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是 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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

小智 5

您不应该期望 RoomLiveData返回任何内容,除非null添加了getValue()RoomObserver并在其回调中收到了第一个值。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
    }
Run Code Online (Sandbox Code Playgroud)

您应该使用Dao具有更直接返回类型的方法来检索值,例如 在你的Dao而不是这样的:

@Query("<your query here>")
fun getWeatherLocation(): LiveData<LocationEntity>
Run Code Online (Sandbox Code Playgroud)

创建并使用其中之一:

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

@Query("<your query here>")
fun getWeatherLocationSync(): LocationEntity?
Run Code Online (Sandbox Code Playgroud)

在检索结果之前不会返回。