Paging library DataSource.Factory for multiple data sources - android

The new paging library allows us to specify a custom data source to use with data pagination. Paging library documentation and sample code on github show us how to create your custom data source instances by creating a subclass of DataSource.Factory like so:
class ConcertTimeDataSourceFactory(private val concertStartTime: Date) :
DataSource.Factory<Date, Concert>() {
val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
override fun create(): DataSource<Date, Concert> {
val source = ConcertTimeDataSource(concertStartTime)
sourceLiveData.postValue(source)
return source
}
}
In a real app, you'd generally have multiple views with recyclerviews and hence multiple custom data sources. So, do you end up creating multiple implementations of DataSource.Factory per data source or is there a more generic solution?

Not always.
If you are using other Android Architecture components or libraries that give it good support, in most cases the DataSource.Factory will be delivered as a result of a method call like Room database does.
If you really want a very generic one and have no problem with reflection:
class GenericFactory<K, R>(private val kClass: KClass<DataSource<K, R>>) : DataSource.Factory<K, R>() {
override fun create(): DataSource<K, R> = kClass.java.newInstance()
}
Your example shows a DataSource.Factory that exposes the DataSource as a LiveData. This is just necessary in specific cases, for example, when the DataSource holds a retry method for the API call. In other cases, your DataSource.Factory will be as simple as 3 more lines in your DataSource:
class MySimpleDataSource<R> : PageKeyedDataSource<String, R>() {
override fun loadBefore(params: LoadParams<String>,
callback: LoadCallback<String, R>) {
// do your thing
}
override fun loadAfter(params: LoadParams<String>,
callback: LoadCallback<String, R>) {
// do your thing
}
override fun loadInitial(params: LoadInitialParams<String>,
callback: LoadInitialCallback<String, R>) {
// do your thing
}
class Factory<R> : DataSource.Factory<String, R>() {
override fun create(): DataSource<String, R> = MySimpleDataSource<R>()
}
}
I guess the most common case for custom DataSource.Factory is paginated REST API calls. In this case, you may just implement one generic DataSource and one DataSource.Factory that receives the request object and response callback as a lambda.
data class MyCollection<R>(
var items: List<R>,
var nextPageToken: String
)
data class MyData(
var title: String = ""
)
abstract class SomeLibraryPagedClientRequest<R> {
abstract fun setNextPageToken(token: String?): SomeLibraryPagedClientRequest<R>
abstract fun enqueue(callback: (response: Response<R>) -> Unit): Unit
}
class MyRestApiDataSource(
private val request: SomeLibraryPagedClientRequest<MyData>,
private val handleResponse: (Response<R>) -> Unit
) : ItemKeyedDataSource<String, MyData>() {
var nextPageToken: String = ""
override fun getKey(item: MyData): String = nextPageToken
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<MyData>) {
}
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<MyData>) {
request.setNextPageToken(params.requestedInitialKey).enqueue { data ->
nextPageToken = response.data.nextPageToken
if(response.isSucefull) callback.onResult(response.data.items)
handleResponse.invoke(response)
}
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<MyData>) {
request.setNextPageToken(params.key).enqueue { response ->
nextPageToken = response.data.nextPageToken
if(response.isSucefull) callback.onResult(response.data.items)
handleResponse.invoke(response)
}
}
class Factory<R>(
private val request: SomeLibraryPagedClientRequest<MyData>,
private val handleResponse: (Response<R>) -> Unit
) : DataSource.Factory<String, R>() {
override fun create(): DataSource<String, R> = MySimpleDataSource<R>()
}
}

We can create multiple instances of DataSource.Factory class which holds multilpe LiveData objects.
First create instance of factory and viewmodel in main activity then write a switch condition or if else ladder for choosing data source from DataSource.Factory class.
In switch condition you need to call factory.create(viewmodel).getLiveData method
For example
switch (service){
case 1:
final Adapter adapter = new Adapter();
factory.create(viewModel.getClass()).getPagedECListLiveData().observe((LifecycleOwner) activity, new Observer<PagedList<ECRecord>>() {
#Override
public void onChanged(#Nullable PagedList<ECRecord> ecRecords) {
Adapter.submitList(ecRecords);
}
});
recyclerView.setAdapter(adapter);
break;
case 2:
final CAdapter cadapter = new CAdapter();
factory.create(viewModel.getClass()).getPagedSTListLiveData().observe((LifecycleOwner) activity, new Observer<PagedList<STRecord>>() {
#Override
public void onChanged(#Nullable PagedList<STRecord> stRecords) {
ECTAdapter.submitList(stRecords);
}
});
recyclerView.setAdapter(cadapter);
break;
}
Happy Coding :)

As seen in the Guide to the app architecture it's advised to have a single source of truth so no matter how many data sources you have you should only have one single source of truth.
Examples used in the Paging Library all rely on this fact and that is why paging library support Room by default. But it don't means you have to use database, as a matter of fact:
In this model, the database serves as the single source of truth, and
other parts of the app access it via the repository. Regardless of
whether you use a disk cache, we recommend that your repository
designate a data source as the single source of truth to the rest of
your app.
P.S: Even if you don't want to designate a single source of truth you don't have to define multiple DataSource you can just implement a custom data source that combine multiple stream of data to create a displayable list of items. For example:
public class MentionKeyedDataSource extends ItemKeyedDataSource<Long, Mention> {
private Repository repository;
...
private List<Mention> cachedItems;
public MentionKeyedDataSource(Repository repository, ..., List<Mention> cachedItems){
super();
this.repository = repository;
...
this.cachedItems = new ArrayList<>(cachedItems);
}
#Override
public void loadInitial(#NonNull LoadInitialParams<Long> params, final #NonNull ItemKeyedDataSource.LoadInitialCallback<Mention> callback) {
Observable.just(cachedItems)
.filter(() -> return cachedItems != null && !cachedItems.isEmpty())
.switchIfEmpty(repository.getItems(params.requestedLoadSize))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
...

Related

How to avoid duplicate functions in MVVM architecture Android?

I'm building a very simple game with Jetpack Compose where I have 3 screens:
HeroesScreen - where I display all heroes in the game. You can select one, or multiple of the same character.
HeroDetailsScreen - where I display more info about a hero. You can select a hero several times, if you want to have that character multiple times.
ShoppingCartScreen - where you increase/decrease the quantity for each character.
Each screen has a ViewModel, and a Repository class:
HeroesScreen -> HeroesViewModel -> HeroesRepository
HeroDetailsScreen -> HeroDetailsViewModel -> HeroDetailsRepository
ShoppingCartScreen -> ShoppingCartViewModel -> ShoppingCartRepository
Each repository has between 8-12 different API calls. However, two of them are present in each repo, which is increase/decrease quantity. So I have the same 2 functions in 3 repository and 3 view model classes. Is there any way I can avoid those duplicates?
I know I can add these 2 functions only in one repo, and then inject an instance of that repo in the other view models, but is this a good approach? Since ShoppingCartRepository is not somehow related to HeroDetailsViewModel.
Edit
All 3 view model and repo classes contain 8-12 functions, but I will share only what's common in all classes:
class ShoppingCartViewModel #Inject constructor(
private val repo: ShoppingCartRepository
): ViewModel() {
var incrementQuantityResult by mutableStateOf<Result<Boolean>>(false)
private set
var decrementQuantityResult by mutableStateOf<Result<Boolean>>(false)
private set
fun incrementQuantity(heroId: String) = viewModelScope.launch {
repo.incrementQuantity(heroId).collect { result ->
incrementQuantityResult = result
}
}
fun decrementQuantity(heroId: String) = viewModelScope.launch {
repo.decrementQuantity(heroId).collect { result ->
decrementQuantityResult = result
}
}
}
And here is the repo class:
class ShoppingCartRepositoryImpl(
private val db: FirebaseFirestore,
): ShoppingCartRepository {
val heroIdRef = db.collection("shoppingCart").document(heroId)
override fun incrementQuantity(heroId: String) = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(1)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
override fun decrementQuantity(heroId: String) = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(-1)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
}
All the other view model classes and repo classes contain their own logic, including these common functions.
I don't use Firebase, but going off of your code, I think you could do something like this.
You don't seem to be using the heroId parameter of your functions so I'm omitting that.
Here's a couple of different strategies for modularizing this:
For a general solution that can work with any Firebase field, you can make a class that wraps a DocumentReference and a particular field in it, and exposes functions to work with it. This is a form of composition.
class IncrementableField(
private val documentReference: DocumentReference,
val fieldName: String
) {
private fun increment(amount: Float) = flow {
try {
emit(Result.Loading)
heroIdRef.update(fieldName, FieldValue.increment(amount)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
fun increment() = increment(1)
fun decrement() = increment(-1)
}
Then your repo becomes:
class ShoppingCartRepositoryImpl(
private val db: FirebaseFirestore,
): ShoppingCartRepository {
val heroIdRef = db.collection("shoppingCart").document(heroId)
val quantity = IncrementableField(heroIdRef, "quantity")
}
and in your ViewModel, can call quantity.increment() or quantity.decrement().
If you want to be more specific to this quantity type, you could create an interface for it and use extension functions for the implementation. (I don't really like this kind of solution because it makes too much stuff public and possibly hard to test/mock.)
interface Quantifiable {
val documentReference: DocumentReference
}
fun Quantifiable.incrementQuantity()(amount: Float) = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(amount)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
fun Quantifiable.incrementQuantity() = incrementQuantity(1)
fun Quantifiable.decrementQuantity() = incrementQuantity(-1)
Then your Repository can extend this interface:
interface ShoppingCartRepository: Quantitfiable {
//... your existing definition of the interface
}
class ShoppingCartRepositoryImpl(
private val db: FirebaseFirestore,
): ShoppingCartRepository {
private val heroIdRef = db.collection("shoppingCart").document(heroId)
override val documentReference: DocumentReference get() = heroIdRef
}

Elegant way of handling error using Retrofit + Kotlin Flow

I have a favorite way of doing network request on Android (using Retrofit). It looks like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): List<User>
}
And in my ViewModel:
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
emit(networkApi.getUsers())
}.asLiveData()
}
Finally, in my Activity/Fragment:
//MyActivity.kt
class MyActivity: AppCompatActivity() {
private viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.usersLiveData.observe(this) {
// Update the UI here
}
}
}
The reason I like this way is because it natively works with Kotlin flow, which is very easy to use, and has a lot of useful operations (flatMap, etc).
However, I am not sure how to elegantly handle network errors using this method. One approach that I can think of is to use Response<T> as the return type of the network API, like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): Response<List<User>>
}
Then in my view model, I can have an if-else to check the isSuccessful of the response, and get the real result using the .body() API if it is successful. But it will be problematic when I do some transformation in my view model. E.g.
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(response.body()) // response.body() will be List<User>
} else {
// What should I do here?
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
Note the comment "What should I do here?". I don't know what to do in that case. I could wrap the responseBody (in this case, a list of Users) with some "status" (or simply just pass through the response itself). But that means that I pretty much have to use an if-else to check the status at every step through the flow transformation chain, all the way up to the UI. If the chain is really long (e.g. I have 10 map or flatMapConcat on the chain), it is really annoying to do it in every step.
What is the best way to handle network errors in this case, please?
You should have a sealed class to handle for different type of event. For example, Success, Error or Loading. Here is some of the example that fits your usecases.
enum class ApiStatus{
SUCCESS,
ERROR,
LOADING
} // for your case might be simplify to use only sealed class
sealed class ApiResult <out T> (val status: ApiStatus, val data: T?, val message:String?) {
data class Success<out R>(val _data: R?): ApiResult<R>(
status = ApiStatus.SUCCESS,
data = _data,
message = null
)
data class Error(val exception: String): ApiResult<Nothing>(
status = ApiStatus.ERROR,
data = null,
message = exception
)
data class Loading<out R>(val _data: R?, val isLoading: Boolean): ApiResult<R>(
status = ApiStatus.LOADING,
data = _data,
message = null
)
}
Then, in your ViewModel,
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
// this should be returned as a function, not a variable
val usersLiveData = flow {
emit(ApiResult.Loading(true)) // 1. Loading State
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(ApiResult.Success(response.body())) // 2. Success State
} else {
val errorMsg = response.errorBody()?.string()
response.errorBody()?.close() // remember to close it after getting the stream of error body
emit(ApiResult.Error(errorMsg)) // 3. Error State
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
In your view (Activity/Fragment), observe these state.
viewModel.usersLiveData.observe(this) { result ->
// Update the UI here
when(result.status) {
ApiResult.Success -> {
val data = result.data <-- return List<User>
}
ApiResult.Error -> {
val errorMsg = result.message <-- return errorBody().string()
}
ApiResult.Loading -> {
// here will actually set the state as Loading
// you may put your loading indicator here.
}
}
}
//this class represent load statement management operation
/*
What is a sealed class
A sealed class is an abstract class with a restricted class hierarchy.
Classes that inherit from it have to be in the same file as the sealed class.
This provides more control over the inheritance. They are restricted but also allow freedom in state representation.
Sealed classes can nest data classes, classes, objects, and also other sealed classes.
The autocomplete feature shines when dealing with other sealed classes.
This is because the IDE can detect the branches within these classes.
*/
ٍٍٍٍٍ
sealed class APIResponse<out T>{
class Success<T>(response: Response<T>): APIResponse<T>() {
val data = response.body()
}
class Failure<T>(response: Response<T>): APIResponse<T>() {
val message:String = response.errorBody().toString()
}
class Exception<T>(throwable: Throwable): APIResponse<T>() {
val message:String? = throwable.localizedMessage
}
}
create extention file called APIResponsrEX.kt
and create extextion method
fun <T> APIResponse<T>.onSuccess(onResult :APIResponse.Success<T>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Success) onResult(this)
return this
}
fun <T> APIResponse<T>.onFailure(onResult: APIResponse.Failure<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Failure<*>)
onResult(this)
return this
}
fun <T> APIResponse<T>.onException(onResult: APIResponse.Exception<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Exception<*>) onResult(this)
return this
}
merge it with Retrofit
inline fun <T> Call<T>.request(crossinline onResult: (response: APIResponse<T>) -> Unit) {
enqueue(object : retrofit2.Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
// success
onResult(APIResponse.Success(response))
} else {
//failure
onResult(APIResponse.Failure(response))
}
}
override fun onFailure(call: Call<T>, throwable: Throwable) {
onResult(APIResponse.Exception(throwable))
}
})
}

Proper way to update LiveData from the Model?

The "proper" way to update views with Android seems to be LiveData. But I can't determine the "proper" way to connect that to a model. Most of the documentation I have seen shows connecting to Room which returns a LiveData object. But (assuming I am not using Room), returning a LiveData object (which is "lifecycle aware", so specific to the activity/view framework of Android) in my model seems to me to violate the separation of concerns?
Here is an example with Activity...
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_activity);
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
val nameText = findViewById<TextView>(R.id.nameTextBox)
viewModel.getName().observe(this, { name ->
nameText.value = name
})
}
}
And ViewModel...
class UserViewModel(): ViewModel() {
private val name: MutableLiveData<String> = MutableLiveData()
fun getName() : LiveData<String> {
return name
}
}
But how do I then connect that to my Model without putting a "lifecycle aware" object that is designed for a specific framework in my model (LiveData)...
class UserModel {
val uid
var name
fun queryUserInfo() {
/* API query here ... */
val request = JSONObjectRequest( ...
{ response ->
if( response.name != this.name ) {
this.name = response.name
/* Trigger LiveData update here somehow??? */
}
}
)
}
}
I am thinking I can maybe put an Observable object in my model and then use that to trigger the update of the LiveData in my ViewModel. But don't find any places where anyone else says that is the "right" way of doing it. Or, can I instantiate the LiveData object in the ViewModel from an Observable object in my model?
Or am I just thinking about this wrong or am I missing something?
This is from official documentation. Check comments in code...
UserModel should remain clean
class UserModel {
private val name: String,
private val lastName: String
}
Create repository to catch data from network
class UserRepository {
private val webservice: Webservice = TODO()
fun getUser(userId: String): LiveData<UserModel > {
val data = MutableLiveData<UserModel>() //Livedata that you observe
//you can get the data from api as you want, but it is important that you
//update the LiveDate that you will observe from the ViewModel
//and the same principle is in the relation ViewModel <=> Fragment
webservice.getUser(userId).enqueue(object : Callback<UserModel > {
override fun onResponse(call: Call<User>, response: Response<UserModel >) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<UserModel >, t: Throwable) {
TODO()
}
})
return data //you will observe this from ViewModel
}
}
The following picture should explain to you what everything looks like
For more details check this:
https://developer.android.com/jetpack/guide
viewmodels-and-livedata-patterns-antipatterns

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

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.
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.

Missing callback to view with pagination?

I'm attempting to get pagination up and workning with Google's new library, but seeing some odd behavior. I'm not sure where I am going wrong.
I'm followig MVP and also using some dagger injection for testability.
In the view:
val adapter = ItemsAdapter()
viewModel.getItems(itemCategoryId, keywords).observe(this, Observer {
Log.d(TAG, "Items updated $it")
adapter.setList(it)
})
The data source factory:
class ItemDataSourceFactory(
private val api: Api,
private val retryExecutor: Executor
) : DataSource.Factory<Long, Item> {
private val mutableLiveData = MutableLiveData<ItemDataSource>()
override fun create(): DataSource<Long, Item> {
val source = ItemDataSource(api, retryExecutor)
mutableLiveData.postValue(source)
return source
}
}
The data source:
class ItemDataSource(
private val api: Api,
private val retryExecutor: Executor
): ItemKeyedDataSource<Long, Item>() {
companion object {
private val TAG = ItemKeyDataSource::class.java
}
override fun getKey(item: Item): Long = item.id
override fun loadBefore(params: LoadParams<Long>, callback: LoadCallback<Item>) {
// ignored, since we only ever append to our initial load
}
override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<Item>) {
api.loadItems(1, params.requestedLoadSize)
.subscribe({
Logger.d(TAG, "Page 1 loaded. Count ${params.requestedLoadSize}.\nItems: ${it.items}")
callback.onResult(it.items as MutableList<Item>, 0, it.item.size)
}, {})
}
override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<Item>) {
api.loadItems(params.key, params.requestedLoadSize)
.subscribe({
Logger.d(TAG, "Page ${params.key} loaded. Count ${params.requestedLoadSize}.\nItems: ${it.items}")
callback.onResult(it.itemsas MutableList<Item>)
}, {})
}
}
And the view model:
class ItemsViewModel #Inject internal constructor(
private val repository: ItemsMvp.Repository
): ViewModel(), ItemsMvp.Model {
override fun items(categoryId: Long, keywords: String?): LiveData<PagedList<Item>> {
return repository.items(categoryId, keywords)
}
}
And the repository layer:
class ItemsRepository #Inject internal constructor(
private val api: Api,
) : ItemsMvp.Repository {
companion object {
const val DEFAULT_THREAD_POOL_SIZE = 5
const val DEFAULT_PAGE_SIZE = 20
}
private val networkExecutor = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE)
private val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(DEFAULT_PAGE_SIZE)
.setPageSize(DEFAULT_PAGE_SIZE)
.build()
override fun items(categoryId: Long, keywords: String?): LiveData<PagedList<Item>> {
val sourceFactory = ItemDataSourceFactory(api, networkExecutor)
// provide custom executor for network requests, otherwise it will default to
// Arch Components' IO pool which is also used for disk access
return LivePagedListBuilder(sourceFactory, pagedListConfig)
.setBackgroundThreadExecutor(networkExecutor)
.build()
}
}
The issue is I'm not getting an update to the view after the first page is loaded.
I see this log from the onCreate():
Items updated []
but then after when the data source returns the items, I see these logs:
Page 1 loaded. Count 20.
Items: [Item(....)]
BUT I never see the view that's subscribing to the view model get an update to set the list on the adapter. If you are curiousI'm using a PagedListAdapter.
I had two mistakes...
The adapter was never set in the view (fail)
I was extending ItemKeyedDataSource instead of PageKeyedDataSource (double fail)
I made those two adjustments and now everything is behaving as expected.

Categories

Resources