Polymorphic entities in Room Polymorphic entities in Room android android

Polymorphic entities in Room


I think you have a number of options here:

Option 1

You use a single table to store all MediaItems and you use a discriminator column to make the difference between a video and a photo.You have a single DAO method that performs the query, applies the order by and returns a Cursor. Then you can use your existing cursor manipulation logic to return a List<MediaItem>It can look like this:

@Daopublic abstract class MediaItemDao() {    @Query("you query here")    protected Cursor getByAlbumIdInternal(int albumId);    public List<MediaItem> getByAbumId(int albumId) {        Cursor cursor = null;        try{            List<MediaItem> mediaList = new ArrayList<>();            cursor = getByAlbumIdInternal(albumId);            // looping through all rows and adding to list            if (cursor.moveToFirst()){                do {                    // use the discriminator value here                    int mediaType = cursor.getInt(cursor.getColumnIndex("you discriminator column name here"));                    MediaItem mediaItem = null;                    if (mediaType == 0) {                        mediaItem = new PhotoMediaItem(cursor.getInt(0), null, palbumId);                    } else if (mediaType == 1) {                        mediaItem = new VideoMediaItem(cursor.getInt(0), null, palbumId, cursor.getLong(3));                    }                    mediaList.add(mediaItem);                } while (cursor.moveToNext());            }            return mediaList;        }        finally  {            if(cursor != null){                cursor.close();            }        }    }}

Option 2

You use two different tables to store VideosMediaItem and PhotosMediaItem. You have a MediaItemDao that has two internal methods to perform the queries and a single public method that merges the two result sets together and applies the sorting in java code. It can look like this:

@Daopublic abstract class MediaItemDao() {    @Query("your query to get the videos, no order by")    protected List<VideoMediaItem> getVideosByAlbumId(int albumId);    @Query("your query to get the photos, no order by")    protected List<PhotosMediaItem> getPhotosByAlbumId(int albumId);    @Transaction    public List<MediaItem> getByAlbumId(int albumId) {        final List<MediaItem> mediaItems = new LinkedList<>();        mediaItems.add(getVideosByAlbumId(albumId));        mediaItems.add(getPhotosByAlbumId(albumId));        Collections.sort(mediaItems, <you can add a comparator here>);        return mediaItems;    }}

EDIT: How to leverage live data for this option?

As I mentioned, you should use LiveData as the return type of your protected methods so you get notified for underlying changes on those tables. So they should look like this:

protected LiveData<List<VideoMediaItem>> getVideosByAlbumId(int albumId);protected LiveData<List<PhotosMediaItem>> getPhotosByAlbumId(int albumId);

In order to return a single LiveData to the client, you should zip the outputs of those two methods into a single stream. You can achieve this using a custom MediatorLiveData implementation. It may look like this:

public class ZipLiveData<T1, T2, R> extends MediatorLiveData<R> {    private T1 mLastLeft;    private T2 mLastRight;    private Zipper<T1, T2, R> mZipper;    public static final <T1, T2, R> LiveData<R> create(@NonNull LiveData<T1> left, @NonNull LiveData<T2> right, Zipper<T1, T2, R> zipper) {        final ZipLiveData<T1, T2, R> liveData = new ZipLiveData(zipper);        liveData.addSource(left, value -> {            liveData.mLastLeft = value;            update();        });        liveData.addSource(right, value -> {            liveData.mLastRight = value;            update();        });        return liveData;    }    private ZipLiveData(@NonNull Zipper<T1, T2, R> zipper) {        mZipper = zipper;    }    private update() {        final R result = zipper.zip(mLastLeft, mLastRight);        setValue(result);    }    public interface Zipper<T1, T2, R> {        R zip(T1 left, T2 right);    }}

Then you just use it in your repository public method like this:

public List<MediaItem> getByAlbumId(int albumId) {    return ZipLiveData.create(        getVideosByAlbumId(albumId),        getPhotosByAlbumId(albumId),        (videos, photos) -> {            final List<MediaItem> mediaItems = new LinkedList<>();            mediaItems.add(videos);            mediaItems.add(photos);            Collections.sort(mediaItems, <you can add a comparator here>);            return mediaItems;        }}

Option 3

This is applicable only if you have a repository layer in place.

You use two different tables to store VideosMediaItem and PhotosMediaItem. You have a DAO class for each one. You have a repository that depends on the both DAOs and combines the result sets, applying the sorting. It can look like this:

@Daopublic abstract class VideosMediaItemDao {    @Query("your query to get the videos, no order by")    public List<VideoMediaItem> getByAlbumId(int albumId);}@Daopublic abstract class PhotosMediaItemDao {    @Query("your query to get the photos, no order by")    public List<PhotosMediaItem> getByAlbymId(int albumId);}public interface MediaItemRepository {    public List<MediaItem> getByAlbumId(int albumId);}class MediaItemRepositoryImpl {    private final VideosMediaItemDao mVideoDao;    private final PhotosMediaItemDao mPhotoDao;    MediaItemRepositoryImpl(VideosMediaItemDao videoDao, PhotosMediaItemDao photoDao) {        mVideoDao = videoDao;        mPhotoDao = photoDao;    }    @Override    public List<MediaItem> getByAlbumId(int albumId) {        final List<MediaItem> mediaItems = new LinkedList<>();        mediaItems.add(mVideoDao.getByAlbumId(albumId));        mediaItems.add(mPhotoDao.getByAlbumId(albumId));        Collections.sort(mediaItems, <you can add a comparator here>);        return mediaItems;    }}


I was playing a bit with it and seems found a generic solution to zip multiple LiveData sources together.

import androidx.lifecycle.LiveDataimport androidx.lifecycle.MediatorLiveDataclass ZipLiveData<S>(private val process: (List<S>) -> List<S>) : MediatorLiveData<List<S>>() {    val map = hashMapOf<String, List<S>>()    fun addSource(source: LiveData<*>) {        addSource(source) {            map[source.toString()] = it as List<S>            value = process(map.values.flatten())        }    }}

And usage:

    @Transaction    fun findItems(albumId: Int): LiveData<List<MediaItem>> {        val liveData = ZipLiveData<MediaItem> { it.sortedBy { item -> item.weight } }        liveData.addSource(getVideosByAlbumId(albumId))        liveData.addSource(getPhotosByAlbumId(albumId))        liveData.addSource(getSoundsByAlbumId(albumId))        return liveData    }

Not sure if it's the most elegant solution though