Search code examples
androidandroid-studioandroid-roomtypeconverterdata-class

Problem in TypeConverters In Room Database


I am trying to use type converters in Android (Kotlin) so i am using the type converters class but i am getting confused like inside of the clouds i am having a single variable so i have returned it but

@Entity(tableName = "WeatherDb")
data class WeatherDTO(
    val base: String,
    val clouds: Clouds,
    val cod: Int,
    val coord: Coord,
    val dt: Int,
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val main: Main,
    val name: String,
    val sys: Sys,
    val timezone: Int,
    val visibility: Int,
    val weather: List<Weather>,
    val wind: Wind
)

class TypeConverters {
    @TypeConverter
    fun fromCloudsToDouble(clouds: Clouds): Int {
        return clouds.all
    }

    fun fromCoordToDouble(coord: Coord): Double {

    }
}

In coord class here are multiple with different datatypes how to covert this?

data class Main(
    val feels_like: Double,
    val grnd_level: Int,
    val humidity: Int,
    val pressure: Int,
    val sea_level: Int,
    val temp: Double,
    val temp_max: Double,
    val temp_min: Double
)

Clouds.kt

data class Clouds(
    val all: Int
)

Coord.kt

data class Coord(
    val lat: Double,
    val lon: Double
)

Main.kt

data class Main(
    val feels_like: Double,
    val grnd_level: Int,
    val humidity: Int,
    val pressure: Int,
    val sea_level: Int,
    val temp: Double,
    val temp_max: Double,
    val temp_min: Double
)

Sys.kt

data class Sys(
    val country: String,
    val id: Int,
    val sunrise: Int,
    val sunset: Int,
    val type: Int
)

Weather.kt

data class Weather(
    val description: String,
    val icon: String,
    val id: Int,
    val main: String
)

Wind.kt

data class Wind(
    val deg: Int,
    val gust: Double,
    val speed: Double
)

WeatherViewModel.kt

@HiltViewModel
class WeatherViewModel @Inject constructor(
    private val repo:WeatherRepository,
    private val application: Application,
    private val WeatherDb:WeatherDB,
    private val fusedLocationProviderClient: FusedLocationProviderClient
) :ViewModel(){

    private val _resp = MutableLiveData<WeatherDTO>()
    val weatherResp:LiveData<WeatherDTO>
    get() = _resp



    private val _cord = MutableLiveData<Coord>()
    val cord:LiveData<Coord>
        get() = _cord

    var locality:String = ""

   fun getWeather(latitude:Double,longitude:Double) =
        viewModelScope.launch {
            repo.getWeather(latitude,longitude).let { response->
                if(response.isSuccessful){
                    Log.d("response","${response.body()}")
                    WeatherDb.WeatherDao().insertWeather(response.body()!!)
                    _resp.postValue(response.body())
                }else{
                    Log.d("Weather Error","getWeather Error Response: ${response.message()}")
                }
            }
        }


    fun fetchLocation():Boolean{
        val task = fusedLocationProviderClient.lastLocation
        if(ActivityCompat.checkSelfPermission(application,android.Manifest.permission.ACCESS_FINE_LOCATION)
            !=PackageManager.PERMISSION_GRANTED &&
            ActivityCompat.checkSelfPermission(application,android.Manifest.permission.ACCESS_COARSE_LOCATION)
            !=PackageManager.PERMISSION_GRANTED
        ){
            return true
        }
        task.addOnSuccessListener {
            if(it!=null){
                getWeather(it.latitude,it.longitude)
                getAddressName(it.latitude,it.longitude)
                Log.d("localityname", locality)
            }
        }
        return true

    }

    private fun fetchLocationDetails(){

    }
    private fun getAddressName(lat:Double,long:Double):String{

        var addressName = " "
        val geoCoder = Geocoder(application, Locale.getDefault())
        val address = geoCoder.getFromLocation(lat,long,1)
        if (address != null) {
            addressName = address[0].adminArea
        }
        locality = addressName

        Log.d("subadmin",addressName.toString())
        Log.d("Address", addressName)
        return addressName


    }

    fun getCoordinates(cord:String){
        val geocoder = Geocoder(application,Locale.getDefault())
        val address = geocoder.getFromLocationName(cord,2)
        val result = address?.get(0)
        if (result != null) {



            getWeather(result.latitude,result.longitude)
            getAddressName(result.latitude,result.longitude)
        }

    }






}

Solution

  • so i am using the type converters class but i am getting confused

    SQLite (the database around which Room is an object orientated wrapper) is not an object orientated (or aware) database. It is a database that can store primitive types of data which are one of

    • INTEGER (such as Int or Long), REAL
    • REAL (such as Float or Double)
    • TEXT (such as String)
    • BLOB (such as ByteArray)
    • NULL

    Therefore to store a type of Coord, Cloud or Weather .... you have three options:-

    1. to embed the class, in which case the fields are copied from the embedded class (would be complicated if the embedded classes contained unsupported types). not covered in the answer
    2. to have the class as a table in it's own right with a relationship between it and the parent (WeatherDTO). not covered in the answer
    3. to convert the class to one of the SQLite types (of which either TEXT or BLOB would probably only be practical).

    Considering option 3 (TyepConverters) converting the data is of little, if any, use just storing the data as you would not be able to retrieve the data.

    As such type converters should always be paired.

    • One of the pair will be to convert from the class to a type that can be stored.
    • The other will be to convert from the stored type to the class.

    As such you will need quite a few type Converters, that is 2 each for fields:-

    • clouds (class Clouds)
    • coord (class Coord)
    • main (class Main)
    • sys (class Sys)
    • weather (class List)
    • wind (class Wind)

    It is the Class of the field that Room looks at to locate the respective type converter.

    One of the simplest ways to convert objects (aka classes) is to convert the object to a JSON representation. Although a complexity with this is that there are many JSON libraries and they will often have differences.

    For the examples that follow Googles JSON library has been used. However, use of this library with Room doesn't appear to directly support the use of List<the_class> e.g. List.

    • The dependency for this being (as an example) implementation 'com.google.code.gson:gson:2.10'

    As a get around a new class WeatherList has ben used as per:-

    data class WeatherList(
        val weatherList: List<Weather>
    )
    

    and the WeatherDTO class has been changed to use it as per :-

    ....
    //val weather: List<Weather>,
    val weather: WeatherList,
    ....
    

    As such the TypeConverters class could then be:-

    class TypeConverters {
        @TypeConverter
        fun fromCloudsToJSONString(clouds: Clouds): String = Gson().toJson(clouds)
        @TypeConverter
        fun toCloudsFromJSONString(jsonString: String): Clouds = Gson().fromJson(jsonString,Clouds::class.java)
        @TypeConverter
        fun fromCoordToJSONString(coord: Coord): String = Gson().toJson(coord)
        @TypeConverter
        fun toCoordFromJSONString(jsonString: String): Coord = Gson().fromJson(jsonString,Coord::class.java)
        @TypeConverter
        fun fromMaintoJSONString(main: Main): String = Gson().toJson(main)
        @TypeConverter
        fun toMainFromJSONString(jsonString: String): Main = Gson().fromJson(jsonString,Main::class.java)
        @TypeConverter
        fun fromSysToJSONString(sys: Sys): String = Gson().toJson(sys)
        @TypeConverter
        fun toSysFromJSONString(jsonString: String): Sys = Gson().fromJson(jsonString,Sys::class.java)
        @TypeConverter
        fun fromWeatherListFromJSONString(weatherList: WeatherList): String = Gson().toJson(weatherList)
        @TypeConverter
        fun toWeatherListFromJSOnString(jsonString: String): WeatherList = Gson().fromJson(jsonString,WeatherList::class.java)
        @TypeConverter
        fun fromWindToJSONString(wind: Wind): String = Gson().toJson(wind)
        @TypeConverter
        fun toWindFromJSONString(jsonString: String): Wind = Gson().fromJson(jsonString,Wind::class.java)
    }
    

    As such the all the types/classes/objects that are not directly supported are converted to/from a JSON string representation of the type/class/object.

    Note that you need to add the @TypeConverters(@TypeConverters( value = [<????>.TypeConverters::class]). Where has to distinguish between your projects TypeConverters class from Room's (TypeConverters is probably not the best name for the class, renaming it, would overcome the need to distinguish)

    Working Example

    The following puts the above into action.

    As the question does not include the underlying classes, the following have been used:-

    data class Coord(
        val longitude: Double,
        val latitude: Double
    )
    data class Clouds(
        val cover: Double,
        val type: String
    )
    data class Main(
        val main: Double
    )
    data class Sys(
        val sys: Double
    )
    data class WeatherList(
        val weatherList: List<Weather>
    )
    data class Weather(
        val weather: Double
    )
    data class Wind(
        val wind: Double
    )
    

    The @Dao annotated interface was also made up and is simply:-

    @Dao
    interface AllDao {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(weatherDTO: WeatherDTO)
        @Query("SELECT * FROM weatherdb")
        fun getAllFromWeatherDB(): List<WeatherDTO>
    }
    

    Also the @Database annotated abstract class was made up it being:-

    @TypeConverters( value = [a.a.so74384736typeconverterconfusion.TypeConverters::class])
    @Database(entities = [WeatherDTO::class], exportSchema = false, version = 1)
    abstract class TheDatabase: RoomDatabase() {
        abstract fun getAllDao(): AllDao
    
        companion object {
            private var instance: TheDatabase? = null
            fun getInstance(context: Context): TheDatabase {
                if (instance==null) {
                    instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                        .allowMainThreadQueries()
                        .build()
                }
                return instance as TheDatabase
            }
        }
    }
    
    • Note the package name used to distinguish the TypeConverters class from Room's TypeConverters class

    • the package name cannot be used elsewhere, so if the above is copied then it would have to be changed. There is no expectation that the code in it's entirety would be copied and used. The code is designed solely to demonstrate the TypeConverters.

    Last some activity code to actually do something (store and retrieve some data):-

    class MainActivity : AppCompatActivity() {
    
        lateinit var db: TheDatabase
        lateinit var dao: AllDao
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            db = TheDatabase.getInstance(this)
            dao = db.getAllDao()
    
            dao.insert(
                WeatherDTO(
                "base001",
                Clouds(25.5,"cumulus"),10,
                    Coord(10.567,30.345),
                    11,
                    12,
                    Main(12345.67890),
                    "thename",
                    Sys(9.87654321),
                    14,
                    1000,
                    WeatherList(listOf(Weather(5.1234),Weather(6.5432), Weather(7.6543))),
                    Wind(23.12)
                )
            )
            for (wdto in dao.getAllFromWeatherDB()) {
                Log.d("DBINFO","base = ${wdto.base} longitude = ${wdto.coord.longitude} latitude = ${wdto.coord.latitude} etc ....")
            }
        }
    }
    

    RESULT

    When run the log contains, as expected:-

    D/DBINFO: base = base001 longitude = 10.567 latitude = 30.345 etc ....
    

    Using App Inspection then the database looks like:-

    enter image description here

    • The fields converted to a JSON string have been highlighted.
    • Obviously the data will very likely not exactly be as you would expect due to the made up classes.