Paging library with custom model class which differs from SQLite model class - Android

Viewed 977

I'm following the Paging Library Overview from Android Developers which uses DataSource.Factory to get the data from the database the next way:

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM Concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

However, in my case, I don't have a table Concert but a table ConcertAA which stores values in a different way.

For example, my Concert class is:

data class Concert(val date: Long, val bands: List<Band>)

Whereas my ConcertAA Active Android class is:

@Table(name = "Concerts")
class ConcertAA(): Model(){

    @Column(name = "Bands")
    var bands: String? = null

    @Column(name = "Date", index = true)
    var date: Long? = null

}

Where I'm saving the bands as a Json String.

Hence, my question is how do I have a ConcertDao where, at the moment of the query to my database, I transform each ConcertAA object into a Concert object to be used in the list? Since the query SELECT * FROM Concerts ORDER BY date DESC will return a list of ConcertAA and not a list of Concert.

1 Answers

Even though the next would answer my own question, for Database-Network logic where the data can mutate, this solution is not enough since it doesn't handle updates to data already saved in database.

Pardon the amount of abstract classes - I usually work for extension and reusability. Moreover, also pardon the mixture of Java and Kotlin code.. for some reason some of the libraries were not recognised in my project using Kotlin but feel free to use Kotlin in those cases.

So basically we needed to create a new DataSource class

abstract class MyDataSource<Value>: PositionalDataSource<Value>() {

    override fun loadInitial(params: PositionalDataSource.LoadInitialParams, callback: PositionalDataSource.LoadInitialCallback<Value>) {
        callback.onResult(getDataFromDatabase(params.requestedLoadSize, params.requestedStartPosition)?: listOf(), 0)
    }

    override fun loadRange(params: PositionalDataSource.LoadRangeParams, callback: PositionalDataSource.LoadRangeCallback<Value>) {
        callback.onResult(getDataFromDatabase(params.loadSize, params.startPosition)?: listOf())
    }

    abstract fun getDataFromDatabase(limit: Int, offset: Int): List<Value>?
}

The implemented Concert data class:

class ConcertDataSource : MyDataSource<Concert>() {

    override fun getDataFromDatabase(limit: Int, offset: Int): List<Concert>? {
        return ConcertAA.getConcerts(limit, offset)
    }
}

Then we need a DataSourceFactory that would be creating our DataSource instance:

abstract class MyDataSourceFactory<Key, Value> : DataSource.Factory<Key, Value>() {

    val mutableLiveData: MutableLiveData<DataSource<Key, Value>>? = null

    override fun create(): DataSource<Key, Value> {
        val dataSource = createDataSource()
        mutableLiveData?.postValue(dataSource)
        return dataSource
    }

    abstract fun createDataSource(): DataSource<Key, Value>
}

And the implemented DataSourceFactory for the ConcertDataSource:

class ConcertDataSourceFactory : MyDataSourceFactory<Int, Concert>() {

    override fun createDataSource(): DataSource<Int, Concert> {
        return ConcertDataSource()
    }
}

Then a ViewModel for the PagedList that will have our datasource:

public class ConcertViewModel extends ViewModel {
    public final LiveData<PagedList<Concert>> concertList;
    private FetchCallback callback;
    private final int pageSize = 10

    PagedList.BoundaryCallback<Concert> boundaryCallback = new PagedList.BoundaryCallback<Concert>(){
        boolean frontLoaded =false;

        public void onZeroItemsLoaded() {
//            callback.fetch(...);
        }

        public void onItemAtFrontLoaded(@NonNull Concert itemAtFront) {
            if(!frontLoaded) {
//                callback.fetch(...);
            }
            frontLoaded = true;
        }

        public void onItemAtEndLoaded(@NonNull Concert itemAtEnd) {
//            callback.fetch(...);
        }
    };

    public ConcertViewModel(FetchCallback callback) {
        this.callback = callback;
        ConcertDataSourceFactory dataSourceFactory = new ConcertDataSourceFactory();
        PagedList.Config config = new PagedList.Config.Builder()
                .setPageSize(pageSize)
                .setInitialLoadSizeHint(pageSize)
                .setEnablePlaceholders(false)
                .build();
        concertList = new LivePagedListBuilder<>(dataSourceFactory, config).setBoundaryCallback(boundaryCallback).build();
    }

    public void refresh() {
        conertList.getValue().getDataSource().invalidate();
    }
}

We use a BoundaryCallback to listen whenever the PagedList requires to load more whenever it reached either of the sides of the list and our database data does't have any extra data stored. This will enable to fetch more data from the server (or any data provider). The fetch is called using a customised callback:

interface FetchCallback{
    fun fetch(limit: Int?, concertIdOffset: String?)
}

Now, since we need to pass a callback to our ViewModel, we need to tell the ViewModelProviders how to create a new instance of ViewModel with parameters. This is done using a ViewModelProvider.Factory:

abstract class MyViewModelFactory(
        private val callback: FetchCallback) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return createViewModel(callback)
    }

    abstract fun <T : ViewModel> createViewModel(callback: FetchCallback): T
}

And our implemented ViewModelFactory for Concert:

class ConcertViewModelFactory(callback: FetchCallback) : MyViewModelFactory(callback) {

    override fun <T : ViewModel> createViewModel(callback: FetchCallback): T {
        return ConcertViewModel(callback) as T
    }
}

Finally, we initialise the ViewModel from our view (Activity, Fragment, etc):

val factory = ConcertViewModelFactory(
            object: FetchCallback{
                override fun fetch(limit: Int?, eventIdOffset: String?, level: RequestParameters.RequestLevel, showLoader: Boolean) {
                    //Do the call to the server
                }
            })

    viewModel = ViewModelProviders.of(this, factory).get(ConcertViewModel::class.java)
    adapter = ConcertAdapter()
    viewModel.concertList.observe(viewLifecycleOwner, Observer(adapter::submitList))

This would be the answer to my question, however, note that (at least at the moment) the Paging Library for Android doesn't handle mutable data. For instance, if a concert has a list of bands that can change (e.g. one of the bands cannot make it to the concert), the logic cannot update the data saved in the database since it only asks for extra data to the server when it reaches either of the boundaries (top or bottom) of the data stored in the database. Hence, for my own purposes, this library is useless.

Related