I try to make the project based on clean architecture so that each layer could have its own data model.
I have a database that contains 2 Entities: "movie" and "details".
#Entity(tableName = "movie")
data class MovieDbModel(
var page: Int,
#PrimaryKey(autoGenerate = false)
var id: Int,
var poster_path: String,
var overview: String,
var original_title: String)
#Entity(tableName = "details")
data class DetailsDbModel (
#PrimaryKey(autoGenerate = false)
val id: Int,
val genres: Genres,
val runtime: Int,
)
I want to map those 2 entities to 1 UI model "MovieAndDetailsUi " and convert it to MutableList that I will use for liveData.
data class MovieAndDetailsUi (
val page: Int,
val id: Int,
val poster_path: String,
val overview: String,
val original_title: String,
val genres: List<GenreUi>,
val runtime: Int,
)
But I have problems with that. Could you please show me the best way to do that? Thank you.
If you want to map those two you could create a companion object on the MoviesAndDetailsUi class that takes both entities and combines them, for example
data class MovieAndDetailsUi (
val page: Int,
val id: Int,
val poster_path: String,
val overview: String,
val original_title: String,
val genres: List<GenreUi>,
val runtime: Int,
) {
companion object {
fun fromDatabaseEntities(movie: MovieDbModel, details: DetailsDbModel) {
return MovieAndDetailsUI(
page = movie.page,
id = movie.id,
poster_path = movie.poster_path,
overview = movie.overview,
original_title = movie.original_title,
genres = details.genres,
runtime = details.runtime
)
}
}
}
That way on your view model you could just get the data from the DB and map it to the UI class, for example:
fun getMovies(): MutableList<MovieAndDetailsUi>() =
viewModelScope.launch() {
withContext(Dispatches.IO) {
val movies: List<MovieDbModel> = ... // Access the DB and get the movies
val details: List<DetailsDbModel = ... // Access the DB and get the details
val zipData = movies.zip(details) {movie, detail -> Pair(movie, detail)}
val mappedData = zipData.map { pair -> MovieAndDetailsUi.fromDatabaseEntities(pair.first, pair.second) }.toMutableList()
liveData.postValue(mappedData)
}
}
I also agree that in this case you should go with the flat structure on the entities as you can skip the mapping and also avoid having multiple classes with the same attributes basically, but in case you want to go this way here's how. Also, I don't know how you are associating the details and movie table and how you query the tables, you might have a problem there too as there's no foreing key on movies referencing to details or vice versa. I added the zip function just for the example but you should have a proper way to associate the movies and the details objects
Related
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)
}
}
}
Here is my converter in the Kotlin:
class Converters {
#TypeConverter
fun valueFromDomainToStorage(value: Value): String {
return value.convertToJson()
}
#TypeConverter
fun valueFromStorageToDomain(str: String): Value {
// we can not create an empty instance of value as TypeDecoder.java should call non-empty constructor
return Value(
"just a stub",
BigInteger.valueOf(0),
BigInteger.valueOf(0),
false,
BigInteger.valueOf(0)
)
.fromJson(str)
}
}
where .convertToJson() and .fromJson(str) implemented as extensions within Value class:
fun Value.convertToJson(): String {
val result = JSONObject()
result.put(ValueConst.OFFER_FIELD, offer)
result.put(ValueConst.AVAILABLE_SINCE, availableSince.toLong())
result.put(ValueConst.AVAILABLE_END, availabilityEnd.toLong())
result.put(ValueConst.IS_CONSUMED, isConsumed)
result.put(ValueConst.LOCKED_UNTIL, lockedUntil)
return result.toString()
}
fun Value.fromJson(json: String): Value {
val subj = JSONObject(json)
return Value(
subj.optString(ValueConst.OFFER_FIELD),
BigInteger.valueOf(subj.optLong(ValueConst.AVAILABLE_SINCE)),
BigInteger.valueOf(subj.optLong(ValueConst.AVAILABLE_END)),
subj.optBoolean(ValueConst.IS_CONSUMED),
BigInteger.valueOf(subj.optLong(ValueConst.LOCKED_UNTIL))
)
}
You should implement Converter class for each non-native class type. Do not forget to register your converters on database:
#Database(entities = [ChainTransaction::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
When you have compile the code and later introduce new changes, you have to increase version parameter too to make changes to take effect:
#Database(entities = [ChainTransaction::class], version = 2, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
Here is official documentation and even training on this topic:
https://developer.android.com/training/data-storage/room
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:-
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
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
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:-
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.
Follow on from the previous answer #Embedded versus Type Converters
As can be seen from the previous answer, there are some issues in regard to using TypeConverters. From a database perspective the TypeConverters will inevitably contain bloat/unecessary data which is contrary to normalisation (not needlessly storing repetitive data).
As an example the JSON representation will for every row contain exactly the same the field names wasting storage, all rows will have the additional overhead of storing the delimiters ([s and ]s, {s and }s, :s ,s). Furthermore actually using the stored data can become complex due to the bloat and also due to multiple values being stored in a single column and as such can be restrictive.
It would be more efficient to not store the bloat and it could eliminate complexities and enhance the usability of the stored data from a database perspective (querying the data for retrieval) to not store multiple values in a single column.
Using the #Embedded annotation can very easily eliminate the bloat. Consider the following (an alternative version of the WeatherDTO class/entity):-
#Entity(tableName = "WeatherDbAlternative1")
data class WeatherDTOAlternative1(
val base: String,
#Embedded
val clouds: Clouds,
val cod: Int,
#Embedded
val coord: Coord,
val dt: Int,
#PrimaryKey(autoGenerate = true)
val id: Int,
#Embedded
val main: Main,
val name: String,
#Embedded
val sys: Sys,
val timezone: Int,
val visibility: Int,
//val weather: List<Weather>,
/* Unable to embed directly so not embedding */
val weather: WeatherList,
#Embedded
val wind: Wind
)
Bar the weather field all that has been done is add the #Embedded annotation. Noting that the classes of the fields all have fields of types directly supported by Room.
Adding this entity to the #Database annotation and adding a couple of additional functions in the #Dao annotated class as per:-
#Query("SELECT * FROM weatherdbalternative1")
fun getAllFromWeatherDBAlternative1(): List<WeatherDTOAlternative1>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(weatherDTOAlternative1: WeatherDTOAlternative1)
And then amending the Activity code to include :-
/*ALTERNATIVE 1 All but WeatherList embedded */
dao.insert(
WeatherDTOAlternative1(
"base001A",
Clouds(25.5, "cumulus"),
10,
Coord(10.567, 30.345),
11,
12,
Main(12345.67890),
"thenameA1",
Sys(9.87654321),
14,
1000,
WeatherList(listOf(Weather(5.1234), Weather(6.5432), Weather(7.6543))),
Wind(23.12)
)
)
for (wdto in dao.getAllFromWeatherDBAlternative1()) {
Log.d(
"DBINFO",
"base = ${wdto.base} longitude = ${wdto.coord.longitude} latitude = ${wdto.coord.latitude} etc ...."
)
}
Now results in the Log including:-
D/DBINFO: base = base001 longitude = 10.567 latitude = 30.345 etc ....
D/DBINFO: base = base001A longitude = 10.567 latitude = 30.345 etc ....
i.e. effectively the same data is stored and retrievable
However the data is now stored in the database as (ignoring the weather field) as :-
i.e. the data stored is much cleaner but at the expense of additional columns (which can be advantageous).
additionally although not apparent, the fields that have the #Embedded annotation do not need the TypeConverters.
I want to add additional field to my new data class from the old one which has an extra field of balance value. I have created data class list with the ID and the Values (balance), which I want to merge first data class list with the second to make 3rd list - similarly like union in sql.
first data class
data class Coin(
val id: String,
val price_usd: Double,
val name: String,
val rank: Int,
val symbol: String
)
second data class
data class CurrencyBalance(
val id: String,
val quantity: String
)
The aim is to create this type of data class list
data class CoinData(
val id: String,
val price_usd: Double,
val name: String,
val rank: Int,
val symbol: String,
val quantity: String
)
Is something like this what you are looking for?
fun convert(currencyBalances: List<CurrencyBalance>, coins: List<Coin>): List<CoinData> =
currencyBalances.map { currencyBalance ->
coins.firstOrNull { coin -> coin.id == currencyBalance.id }
?.let { coin -> CoinData(coin.id, coin.price_usd, coin.name, coin.rank, coin.symbol, currencyBalance.value) }
}.filterNotNull()
I assumed here that the fields in a CurrencyBalance are called id and value
I'm learning Compose by the article.
The article tell me:
All data types that are added to the Bundle are saved automatically. The simplest solution is to add the #Parcelize annotation to the object.
And it gives me the sample code.
#Parcelize
data class City(val name: String, val country: String) : Parcelable
#Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
The following code is from the project.
I find the data class Post doesn't add #Parcelize, and val featured = remember { PostRepo.getFeaturedPost() } is OK, why?
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
...
}
object PostRepo {
fun getPosts(): List<Post> = posts
fun getFeaturedPost(): Post = posts.random()
}
#Immutable
data class Post(
val id: Long,
val title: String,
val subtitle: String? = null,
val url: String,
val metadata: Metadata,
#DrawableRes val imageId: Int,
#DrawableRes val imageThumbId: Int,
val tags: Set<String>
)
#Immutable
data class Metadata(
val author: PostAuthor,
val date: String,
val readTimeMinutes: Int
)
#Immutable
data class PostAuthor(
val name: String,
val url: String? = null
)
While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you use rememberSaveable. RememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.
Since RememberSaveable automatically saves any value that can be saved in a Bundle , it requires the class to be Parcelable unlike remember .
I want to save in my Room database an object where one of the variables can either be of on type or another. I thought a sealed class would make sense, so I took this approach:
sealed class BluetoothMessageType() {
data class Dbm(
val data: String
) : BluetoothMessageType()
data class Pwm(
val data: String
) : BluetoothMessageType()
}
Or even this, but it is not necessary. I found that this one gave me even more errors as it did not know how to handle the open val, so if I find a solution for the first version I would be happy anyway.
sealed class BluetoothMessageType(
open val data: String
) {
data class Dbm(
override val data: String
) : BluetoothMessageType()
data class Pwm(
override val data: String
) : BluetoothMessageType()
}
Then the Entity class
#Entity(tableName = MESSAGES_TABLE_NAME)
data class DatabaseBluetoothMessage(
#PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val time: Long = Instant().millis,
val data: BluetoothMessageType
)
I have created a TypeConverter to convert it to and from a String as well, so I assume that it is not a problem.
First, is this possible? I assume this should function in a similar way that it would with an abstract class, but I have not managed to find a working solution with that either. If it is not possible, what sort of approach should I take when I want to save some data that may be either of one or another type if not with sealed classes?
We faced such problem when we tried using Polymorphism in our domain, and we solved it this way:
Domain:
We have a Photo model that looks like this:
sealed interface Photo {
val id: Long
data class Empty(
override val id: Long
) : Photo
data class Simple(
override val id: Long,
val hasStickers: Boolean,
val accessHash: Long,
val fileReferenceBase64: String,
val date: Int,
val sizes: List<PhotoSize>,
val dcId: Int
) : Photo
}
Photo has PhotoSize inside, it looks like this:
sealed interface PhotoSize {
val type: String
data class Empty(
override val type: String
) : PhotoSize
data class Simple(
override val type: String,
val location: FileLocation,
val width: Int,
val height: Int,
val size: Int,
) : PhotoSize
data class Cached(
override val type: String,
val location: FileLocation,
val width: Int,
val height: Int,
val bytesBase64: String,
) : PhotoSize
data class Stripped(
override val type: String,
val bytesBase64: String,
) : PhotoSize
}
Data:
There is much work to do in our data module to make this happen. I will decompose the process to three parts to make it look easier:
1. Entity:
So, using Room and SQL in general, it is hard to save such objects, so we had to come up with this idea. Our PhotoEntity (Which is the Local version of Photo from our domain looks like this:
#Entity
data class PhotoEntity(
// Shared columns
#PrimaryKey
val id: Long,
val type: Type,
// Simple Columns
val hasStickers: Boolean? = null,
val accessHash: Long? = null,
val fileReferenceBase64: String? = null,
val date: Int? = null,
val dcId: Int? = null
) {
enum class Type {
EMPTY,
SIMPLE,
}
}
And our PhotoSizeEntity looks like this:
#Entity
data class PhotoSizeEntity(
// Shared columns
#PrimaryKey
#Embedded
val identity: Identity,
val type: Type,
// Simple columns
#Embedded
val locationLocal: LocalFileLocation? = null,
val width: Int? = null,
val height: Int? = null,
val size: Int? = null,
// Cached and Stripped columns
val bytesBase64: String? = null,
) {
data class Identity(
val photoId: Long,
val sizeType: String
)
enum class Type {
EMPTY,
SIMPLE,
CACHED,
STRIPPED
}
}
Then we have this compound class to unite PhotoEntity and PhotoSizeEntity together, so we can retrieve all data required by our domain's model:
data class PhotoCompound(
#Embedded
val photo: PhotoEntity,
#Relation(entity = PhotoSizeEntity::class, parentColumn = "id", entityColumn = "photoId")
val sizes: List<PhotoSizeEntity>? = null,
)
2. Dao
So our dao should be able to store and retrieve this data. You can have two daos for PhotoEntity and PhotoSizeEntity instead of one, for the sake of flexibility, but in this example we will use a shared one, it looks like this:
#Dao
interface IPhotoDao {
#Transaction
#Query("SELECT * FROM PhotoEntity WHERE id = :id")
suspend fun getPhotoCompound(id: Long): PhotoCompound
#Transaction
suspend fun insertOrUpdateCompound(compound: PhotoCompound) {
compound.sizes?.let { sizes ->
insertOrUpdate(sizes)
}
insertOrUpdate(compound.photo)
}
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(entity: PhotoEntity)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(entities: List<PhotoSizeEntity>)
}
3. Adapter:
After solving the problem of saving data to SQL database, we now need to solve the problem of converting between domain and local entities. Our Photo's converter aka adapter looks like this:
fun Photo.toCompound() = when(this) {
is Photo.Empty -> this.toCompound()
is Photo.Simple -> this.toCompound()
}
fun PhotoCompound.toModel() = when (photo.type) {
PhotoEntity.Type.EMPTY -> Photo.Empty(photo.id)
PhotoEntity.Type.SIMPLE -> this.toSimpleModel()
}
private fun PhotoCompound.toSimpleModel() = photo.run {
Photo.Simple(
id,
hasStickers!!,
accessHash!!,
fileReferenceBase64!!,
date!!,
sizes?.toModels()!!,
dcId!!
)
}
private fun Photo.Empty.toCompound(): PhotoCompound {
val photo = PhotoEntity(
id,
PhotoEntity.Type.EMPTY
)
return PhotoCompound(photo)
}
private fun Photo.Simple.toCompound(): PhotoCompound {
val photo = PhotoEntity(
id,
PhotoEntity.Type.SIMPLE,
hasStickers = hasStickers,
accessHash = accessHash,
fileReferenceBase64 = fileReferenceBase64,
date = date,
dcId = dcId,
)
val sizeEntities = sizes.toEntities(id)
return PhotoCompound(photo, sizeEntities)
}
And for the PhotoSize, it looks like this:
fun List<PhotoSize>.toEntities(photoId: Long) = map { photoSize ->
photoSize.toEntity(photoId)
}
fun PhotoSize.toEntity(photoId: Long) = when(this) {
is PhotoSize.Cached -> this.toEntity(photoId)
is PhotoSize.Empty -> this.toEntity(photoId)
is PhotoSize.Simple -> this.toEntity(photoId)
is PhotoSize.Stripped -> this.toEntity(photoId)
}
fun List<PhotoSizeEntity>.toModels() = map { photoSizeEntity ->
photoSizeEntity.toModel()
}
fun PhotoSizeEntity.toModel() = when(type) {
PhotoSizeEntity.Type.EMPTY -> this.toEmptyModel()
PhotoSizeEntity.Type.SIMPLE -> this.toSimpleModel()
PhotoSizeEntity.Type.CACHED -> this.toCachedModel()
PhotoSizeEntity.Type.STRIPPED -> this.toStrippedModel()
}
private fun PhotoSizeEntity.toEmptyModel() = PhotoSize.Empty(identity.sizeType)
private fun PhotoSizeEntity.toCachedModel() = PhotoSize.Cached(
identity.sizeType,
locationLocal?.toModel()!!,
width!!,
height!!,
bytesBase64!!
)
private fun PhotoSizeEntity.toSimpleModel() = PhotoSize.Simple(
identity.sizeType,
locationLocal?.toModel()!!,
width!!,
height!!,
size!!
)
private fun PhotoSizeEntity.toStrippedModel() = PhotoSize.Stripped(
identity.sizeType,
bytesBase64!!
)
private fun PhotoSize.Cached.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.CACHED,
locationLocal = location.toEntity(),
width = width,
height = height,
bytesBase64 = bytesBase64
)
private fun PhotoSize.Simple.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.SIMPLE,
locationLocal = location.toEntity(),
width = width,
height = height,
size = size
)
private fun PhotoSize.Stripped.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.STRIPPED,
bytesBase64 = bytesBase64
)
private fun PhotoSize.Empty.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.EMPTY
)
That's it!
Conclusion:
To save a sealed class to Room or SQL, whether as an Entity, or as an Embedded object, you need to have one big data class with all the properties, from all the sealed variants, and use an Enum type to indicate variant type to use later for conversion between domain and data, or for indication in your code if you don't use Clean Architecture. Hard, but solid and flexible. I hope Room will have some annotations that can generate such code to get rid of the boilerplate code.
PS: This class is taken from Telegram's scheme, they also solve the problem of polymorphism when it comes to communication with a server. Checkout their TL Language here: https://core.telegram.org/mtproto/TL
PS2: If you like Telegram's TL language, you can use this generator to generate Kotlin classes from scheme.tl files: https://github.com/tamimattafi/mtproto
EDIT: You can use this code generating library to automatically generate Dao for compound classes, to make it easier to insert, which removes a lot of boilerplate to map things correctly.
Link: https://github.com/tamimattafi/android-room-compound
Happy Coding!
In my case I did the following:
sealed class Status2() {
object Online : Status2()
object Offline : Status2()
override fun toString(): String {
return when (this) {
is Online ->"Online"
is Offline -> "Offline"
}
}
}
class StatusConverter{
#TypeConverter
fun toHealth(value: Boolean): Status2 {
return if (value){
Status2.Online
} else{
Status2.Offline
}
}
#TypeConverter
fun fromHealth(value: Status2):Boolean {
return when(value){
is Status2.Offline -> false
is Status2.Online -> true
}
}
}
#Dao
interface CourierDao2 {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertStatus(courier: CourierCurrentStatus)
#Query("SELECT * FROM CourierCurrentStatus")
fun getCourierStatus(): Flow<CourierCurrentStatus>
}
#Entity
data class CourierCurrentStatus(
#PrimaryKey
val id: Int = 0,
var status: Status2 = Status2.Offline
)
and it works like a charm
I want to merge two list with different type and create and another list which contains data from both lists. Also, merging should only happen when identifier match(like id match in both list)
Employee(val employeeId: Int,
val employeeName: String,
val address: String)
LaptopInfo(val laptopId: Int,
val employeeId: String,
val team: String)
EmployeeLaptopInfo(val laptopId: Int,
val employeeId: String,
val employeeName: String,
val address: String)
So there is list of Employee and LaptopInfo and I want to add EmployeeLaptopInfo to List<EmployeeLaptopInfo> only when employeeId match in both lists
val employeeLaptopInfoList = mutableListOf<EmployeeLaptopInfo>()
val employeeIds = employeeList.map { it.employeeId }.toSet()
lapTopInfoList.filter { employeeIds.contains(it.employeeId) }
.map {
val employeeLaptopInfo = EmployeeLaptopInfo(it.laptopId, laptopId.employeeId, "can't get info", "can't get info")
employeeLaptopInfoList.add(employeeLaptopInfo)
}
But this way I don't get details from both list. Is there any way to do it without using for loop and in efficient manner.
I've optimized Yoni Gibbs's solution. The idea is to filter the laptops in which the employeeId is present in Employee. Then transform each laptop into EmployeeLaptopInfo.
data class Employee(val employeeId: Int, val employeeName: String, val address: String)
data class LaptopInfo(val laptopId: Int, val employeeId: Int, val team: String)
data class EmployeeLaptopInfo(val laptopId: Int, val employeeId: Int, val employeeName: String, val address: String)
fun main(args: Array<String>) {
val employees = listOf<Employee>()
val laptops = listOf<LaptopInfo>()
val employeesById: Map<Int, Employee> = employees.associateBy { it.employeeId }
val result = laptops.filter { employeesById[it.employeeId] != null }.map { laptop ->
employeesById[laptop.employeeId]?.let { employee ->
EmployeeLaptopInfo(laptop.laptopId, laptop.employeeId, employee.employeeName, employee.address)
}
}
}
I think your classes have errors in the data type of some properties, so I changed them like this:
class Employee(val employeeId: Int, val employeeName: String, val address: String)
class LaptopInfo(val laptopId: Int, val employeeId: Int, val team: String)
class EmployeeLaptopInfo(val laptopId: Int, val employeeId: Int, val employeeName: String, val address: String)
If these are the initial lists:
val employeeList = mutableListOf<Employee>()
val laptopInfoList = mutableListOf<LaptopInfo>()
then you map employeeList by finding corresponding values for employeeId in both lists:
val employeeLaptopInfoList =
employeeList.mapNotNull { emp ->
val laptop = laptopInfoList.find { it.employeeId == emp.employeeId }
if (laptop == null) null
else EmployeeLaptopInfo(
laptop.laptopId,
emp.employeeId,
emp.employeeName,
emp.address)
}
I think it might be best to first create two maps, one for the laptops and one for the employees, where both are keyed on the employeeId. Then you can do the following:
val employees = listOf<Employee>()
val laptops = listOf<LaptopInfo>()
val laptopsByEmployeeId: Map<Int, List<LaptopInfo>> = laptops.groupBy { it.employeeId }
val employeesById: Map<Int, Employee> = employees.associateBy { it.employeeId }
val result = laptopsByEmployeeId.flatMap { (employeeId, laptops) ->
laptops.mapNotNull { laptop ->
employeesById[employeeId]?.let { employee ->
EmployeeLaptopInfo(laptop.laptopId, employeeId, employee.employeeName, employee.address)
}
}
}
This means that you're never looping round one list inside a loop round another list, which could be inefficient. Instead, you're using the maps to get at items quickly by their IDs.
(Also, as per the answer from forpas, I'm assuming that the data type for employeeId should be the same in all the classes, e.g. Int.)