Androidew1267
Androidew1267

Reputation: 623

Android Room: offline caching doesn't work

I have followed this course trying to implement online caching in my app. Although everything looks similar, when I turn on airplane mode, data doesn't show. Here is my code:

Repository

class WeatherRepository(private val database: WeatherDatabase) {

    var weather: LiveData<CurrentWeather> = Transformations.map(database.weatherDao.getWeather()){
        it.asDomainModel()
    }

    suspend fun refreshWeather(city: String){
        withContext(Dispatchers.IO){
            val weather = WeatherNetwork.service.getCurrentWeather("London", "metric")
            database.weatherDao.insert(weather.asDatabaseModel())
            Log.i("INSERTING: ", weather.asDatabaseModel().toString())
        }
    }

    //I've created this function only for debugging
    fun getDataFromDB(){
        weather = Transformations.map(database.weatherDao.getWeather()){
            it.asDomainModel()
        }
        val data = database.weatherDao.getWeather()
        Log.i("DB VALUES: ", data.value.toString())
        Log.i("VAR WEATHER IN REPO: ", weather.value.toString())
    }
}

Room Database

@Dao
interface WeatherDao{

    @Query("select * from current_weather")
    fun getWeather(): LiveData<DatabaseWeather>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(weather: DatabaseWeather)
}

@Database(entities = [DatabaseWeather::class], version = 1)
abstract class WeatherDatabase: RoomDatabase(){
    abstract val weatherDao: WeatherDao
}

private lateinit var INSTANCE: WeatherDatabase

fun getDatabase(context: Context): WeatherDatabase{
    synchronized(WeatherDatabase::class.java){
        if (!::INSTANCE.isInitialized){
            INSTANCE = Room.databaseBuilder(context.applicationContext,
            WeatherDatabase::class.java, "weather_db").build()
        }
    }
    return INSTANCE
}

Entity

const val WEATHER_ID = 0

@Entity(tableName = "current_weather")
data class DatabaseWeather constructor(
    val name: String,
    val lon: Double,
    val lat: Double,
    val description: String,
    val icon: String,
    val temp: Double,
    val tempFeelsLike: Double,
    val humidity: Int,
    val pressure: Int
){
    @PrimaryKey(autoGenerate = false)
    var id: Int = WEATHER_ID
}

fun DatabaseWeather.asDomainModel(): CurrentWeather{
    return CurrentWeather(
        name = this.name,
        lon = this.lon,
        lat = this.lat,
        description = this.description,
        icon = this.icon,
        temp = this.temp,
        tempFeelsLike = this.tempFeelsLike,
        humidity = this.humidity,
        pressure = this.pressure
    )
}

Network

const val BASE_URL = "https://api.openweathermap.org/data/2.5/"
const val API_KEY = "xxx"

private val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

interface WeatherApiService {
    @GET("weather")
    suspend fun getCurrentWeather(
        @Query("q") cityName: String,
        @Query("units") units: String
    ): WeatherResponse
}

object WeatherNetwork{
    private val requestInterceptor = Interceptor { chain ->
        val url = chain.request()
            .url()
            .newBuilder()
            .addQueryParameter("appid", API_KEY)
            .build()

        val request = chain.request()
            .newBuilder()
            .url(url)
            .build()

        return@Interceptor chain.proceed(request)
    }

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(requestInterceptor)
        .build()

    private val retrofit = Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()

    val service = retrofit.create(WeatherApiService::class.java)
}

ViewModel

enum class WeatherApiStatus { LOADING, ERROR, DONE }

class CurrentWeatherViewModel(application: Application) : AndroidViewModel(application) {

    val place = MutableLiveData<String>()

    private val _status = MutableLiveData<WeatherApiStatus>()

    val status: LiveData<WeatherApiStatus>
        get() = _status

    private val weatherRepository = WeatherRepository(getDatabase(application))

    val weather = weatherRepository.weather

    init {
        refreshDataFromRepository()
    }

    fun refreshDataFromRepository(){
        viewModelScope.launch {
            try{
                weatherRepository.refreshWeather(place.value.toString())
                _status.value = WeatherApiStatus.DONE
            } catch (e: Exception){
                Log.i("FAILURE: ", e.printStackTrace().toString())
                _status.value = WeatherApiStatus.ERROR
            }
        }
    }

    fun checkDB(){
        weatherRepository.getDataFromDB()
    }
}

It is working, when I don't call method refreshWeather in WeatherRepository (the data is downloaded from db). I don't know why, but logs "DB VALUES" and "VAR WEATHER IN REPO" always show null ("INSERTING" shows data properly)

Upvotes: 0

Views: 562

Answers (1)

Ricardo Carvalho
Ricardo Carvalho

Reputation: 96

The problem you're getting when trying to log the values retrieved from room DB and it aways shows null, is because the database retrieving process always happens asynchronously and the data is not ready yet when you try to log it.

As you can see, DAO's getWeather() method returns a LiveData, and you try to log its value right after asking for it, not giving enough time to fetch it from disk, so resulting in a still null value.

You should observe this LiveData in order to be notified when the value has been retrieved with success from the database, because of the IO nature of this kind of operation (disk access is considerably very slow when compared to the speeds of CPU and RAM memory, where code and variables reside).

Only for debugging purposes (this code would cause many troubles, such as performance and memory issues because it keeps listening the LiveData forever, it's advised to observe with the observe method passing a LifecycleOwner instead of observing forever), you could do it like this:

    fun getDataFromDB(){
        weather = Transformations.map(database.weatherDao.getWeather()){
            it.asDomainModel()
        }
        val data = database.weatherDao.getWeather()

        data.observeForever { value ->
            Log.i("DB VALUES: ", value.toString())
        }
        weather.observeForever { value ->
            Log.i("VAR WEATHER IN REPO: ", value.toString())
        }
    }

Another way to achieve what you want is by changing the return type of the DAO function to return DatabaseWeather directly, making it a suspend fun, like so:

suspend fun getWeather(): DatabaseWeather

but then you would need to change a bunch of other code, just to "wait" for the value to be fetched and ready before being logged (like in the "INSERTING" case, where the code waits on the Coroutine for the WeatherNetwork call, because of the "suspend" functions in there).

Upvotes: 1

Related Questions