I create application based on the Database + Network paging and GitHub rest api.
Using various tutorials, I came to the conclusion that when creating the LivePagedListBuilder in ViewModel, I must pass my query retrieving data from Room, to make it works then with BoundaryCallback.
This query in my code looks like this:
#Query("SELECT * from repositories_table ORDER BY name DESC")
fun getPagedRepos(): DataSource.Factory<Int,Repository>
and its equivalent in the repository:
fun getPagedRepos(): DataSource.Factory<Int, Repository> {
return repositoriesDao.getPagedRepos()
}
However I would like to combine this with my own DataSource, not default one, which would also work with retrofitting data fetching.
Below are the relevant parts of my application:
DataSource
class ReposDataSource(private val contactsRepository: ContactsRepository,
private val scope: CoroutineScope, application: Application): PageKeyedDataSource<Int, Repository>() {
private var supervisorJob = SupervisorJob()
private val PREFS_NAME = "Paging"
private val sharedPref: SharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Repository>
) {
Log.i("RepoBoundaryCallback", "initialTriggered")
val currentPage = 1
val nextPage = currentPage + 1
executeQuery(currentPage, params.requestedLoadSize) {
callback.onResult(it, null, nextPage)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Repository>) {
val currentPage = params.key
val nextPage = currentPage + 1
executeQuery(currentPage, params.requestedLoadSize) {
callback.onResult(it, nextPage)
}
}
override fun invalidate() {
super.invalidate()
supervisorJob.cancelChildren()
}
private fun executeQuery(page: Int, perPage: Int, callback: (List<Repository>) -> Unit) {
scope.launch(getJobErrorHandler() + supervisorJob) {
savePage("current_page", page)
val repos = contactsRepository.fetchPagedRepos(page, perPage)
callback(repos)
}
}
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
Log.e(ReposDataSource::class.java.simpleName, "An error happened: $e")
}
private fun savePage(KEY_NAME: String, value: Int){
Log.i("RepoBoundaryCallback", value.toString())
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putInt(KEY_NAME, value)
editor.commit()
}
}
BoundaryCallback
class RepoBoundaryCallback (val repository: ContactsRepository, application: Application) :
PagedList.BoundaryCallback<Repository?>() {
private var callbackJob = Job()
private val coroutineScope = CoroutineScope(
callbackJob + Dispatchers.Main )
private val PREFS_NAME = "Paging"
private val sharedPref: SharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
override fun onZeroItemsLoaded() {
Log.i("RepoBoundaryCallback", "onzeroitemstriggered")
super.onZeroItemsLoaded()
fetchUsers(1)
}
override fun onItemAtEndLoaded(itemAtEnd: Repository) {
Log.i("RepoBoundaryCallback", "onitematendriggered")
super.onItemAtEndLoaded(itemAtEnd)
fetchUsers(getCurrentPage("current_page"))
}
private fun fetchUsers(page: Int) {
coroutineScope.launch {
try {
var newRepos = RepoApi.retrofitService.fetchRepos(page)
insertRepoToDb(newRepos)
}
catch (e: Exception){
Log.i("RepoBoundaryCallback", e.toString())
}
}
}
private suspend fun insertRepoToDb(reposList: List<Repository>){
reposList.forEach{repository.insertRepo(it)}
}
private fun getCurrentPage(KEY_NAME: String): Int{
return sharedPref.getInt(KEY_NAME, 0)
}
}
Api query
interface RepoApiService {
#GET("/orgs/google/repos")
suspend fun fetchRepos(#Query("page") page: Int,
#Query("per_page") perPage: Int = 15): List<Repository>
}
ViewModel
class RepositoryViewModel (application: Application) : AndroidViewModel(application) {
companion object{
private const val TAG = "RepositoryViewModel"
}
//var reposList: LiveData<PagedList<Repository>>
private var repoBoundaryCallback: RepoBoundaryCallback? = null
var reposList: LiveData<PagedList<Repository>>? = null
private val repository: ContactsRepository
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main )
init {
val contactsDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).contactsDao()
val contactsExtrasDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).contactsExtrasDao()
val repositoriesDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).repositoriesDao()
val service = RepoApi.retrofitService
repository = ContactsRepository(contactsDao, contactsExtrasDao, repositoriesDao, service)
initializedPagedListBuilder(application)
}
private fun initializedPagedListBuilder(application: Application) {
repoBoundaryCallback = RepoBoundaryCallback(
repository, application
)
val pagedListConfig = PagedList.Config.Builder()
//.setPrefetchDistance(5)
//.setInitialLoadSizeHint(20)
.setEnablePlaceholders(true)
.setPageSize(15).build()
reposList = LivePagedListBuilder(
repository.getPagedRepos(),
pagedListConfig
).setBoundaryCallback(repoBoundaryCallback).build()
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
In addition, I save the relevant pages in SharedPreferences in the DataSource to then use it in the corresponding BoundaryCallback functions.
So how do you link your own DataSource to BoundaryCallback with Room and Retrofit? I will be grateful for any help.
BoundaryCallback is responsible for triggering invalidation on your current generation of DataSource. With DataSource.Factory generated by Room, this is automatically handled for you as Room will invalidate any DataSource it generates that is affected by writes to DB. This is why a DataSource.Factory is necessary over a single DataSource. Paging sees a single instance of DataSource as a "snapshot" of static data. If the data it's supposed to be loading changes in any way you must call DataSource.invalidate() to allow DataSource.Factory to generate a new up-to-date snapshot.
Since you're implementing your own DataSource, you'll also need to implement a DataSource.Factory and call invalidate() from your BoundaryCallback (doesn't necessarily need to be in the same class, but invalidate() must be triggered when your BoundaryCallback writes updates).
Related
I am learning Android development, and as I saw in many topics, people were talking about that LiveData is not recommended to use anymore. I mean it's not up-to-date, and we should use Flows instead.
I am trying to get data from ROOM database with Flows and then convert them to StateFlow because as I know they are observables, and I also want to add UI states to them. Like when I get data successfully, state would change to Success or if it fails, it changes to Error.
I have a simple app for practicing. It stores subscribers with name and email, and show them in a recyclerview.
I've checked a lot of sites, how to use stateIn method, how to use StateFlows and Flows but didn't succeed. What's the most optimal way to do this?
And also what's the proper way of updating recyclerview adapter? Is it okay to change it all the time in MainActivity to a new adapter?
Here is the project (SubscriberViewModel.kt - line 30):
Project link
If I am doing other stuff wrong, please tell me, I want to learn. I appreciate any kind of help.
DAO:
import androidx.room.*
import kotlinx.coroutines.flow.Flow
#Dao
interface SubscriberDAO {
#Insert
suspend fun insertSubscriber(subscriber : Subscriber) : Long
#Update
suspend fun updateSubscriber(subscriber: Subscriber) : Int
#Delete
suspend fun deleteSubscriber(subscriber: Subscriber) : Int
#Query("DELETE FROM subscriber_data_table")
suspend fun deleteAll() : Int
#Query("SELECT * FROM subscriber_data_table")
fun getAllSubscribers() : Flow<List<Subscriber>>
#Query("SELECT * FROM subscriber_data_table WHERE :id=subscriber_id")
fun getSubscriberById(id : Int) : Flow<Subscriber>
}
ViewModel:
class SubscriberViewModel(private val repository: SubscriberRepository) : ViewModel() {
private var isUpdateOrDelete = false
private lateinit var subscriberToUpdateOrDelete: Subscriber
val inputName = MutableStateFlow("")
val inputEmail = MutableStateFlow("")
private val _isDataAvailable = MutableStateFlow(false)
val isDataAvailable : StateFlow<Boolean>
get() = _isDataAvailable
val saveOrUpdateButtonText = MutableStateFlow("Save")
val deleteOrDeleteAllButtonText = MutableStateFlow("Delete all")
/*
//TODO - How to implement this as StateFlow<SubscriberListUiState> ??
//private val _subscribers : MutableStateFlow<SubscriberListUiState>
//val subscribers : StateFlow<SubscriberListUiState>
get() = _subscribers
*/
private fun clearInput() {
inputName.value = ""
inputEmail.value = ""
isUpdateOrDelete = false
saveOrUpdateButtonText.value = "Save"
deleteOrDeleteAllButtonText.value = "Delete all"
}
fun initUpdateAndDelete(subscriber: Subscriber) {
inputName.value = subscriber.name
inputEmail.value = subscriber.email
isUpdateOrDelete = true
subscriberToUpdateOrDelete = subscriber
saveOrUpdateButtonText.value = "Update"
deleteOrDeleteAllButtonText.value = "Delete"
}
fun saveOrUpdate() {
if (isUpdateOrDelete) {
subscriberToUpdateOrDelete.name = inputName.value
subscriberToUpdateOrDelete.email = inputEmail.value
update(subscriberToUpdateOrDelete)
} else {
val name = inputName.value
val email = inputEmail.value
if (name.isNotBlank() && email.isNotBlank()) {
insert(Subscriber(0, name, email))
}
inputName.value = ""
inputEmail.value = ""
}
}
fun deleteOrDeleteAll() {
if (isUpdateOrDelete) {
delete(subscriberToUpdateOrDelete)
} else {
deleteAll()
}
}
private fun insert(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(subscriber)
_isDataAvailable.value = true
}
private fun update(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.update(subscriber)
clearInput()
}
private fun delete(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(subscriber)
clearInput()
}
private fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
//_subscribers.value = SubscriberListUiState.Success(emptyList())
_isDataAvailable.value = false
}
sealed class SubscriberListUiState {
data class Success(val list : List<Subscriber>) : SubscriberListUiState()
data class Error(val msg : String) : SubscriberListUiState()
}
}
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: SubscriberViewModel
private lateinit var viewModelFactory: SubscriberViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dao = SubscriberDatabase.getInstance(application).subscriberDAO
viewModelFactory = SubscriberViewModelFactory(SubscriberRepository(dao))
viewModel = ViewModelProvider(this, viewModelFactory)[SubscriberViewModel::class.java]
binding.viewModel = viewModel
binding.lifecycleOwner = this
initRecycleView()
}
private fun initRecycleView() {
binding.recyclerViewSubscribers.layoutManager = LinearLayoutManager(
this#MainActivity,
LinearLayoutManager.VERTICAL, false
)
displaySubscribersList()
}
private fun displaySubscribersList() {
/*
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.subscribers.collect { uiState ->
when (uiState) {
is SubscriberViewModel.SubscriberListUiState.Success -> {
binding.recyclerViewSubscribers.adapter = SubscriberRecyclerViewAdapter(uiState.list) {
subscriber: Subscriber -> listItemClicked(subscriber)
}
}
is SubscriberViewModel.SubscriberListUiState.Error -> {
Toast.makeText(applicationContext,uiState.msg, Toast.LENGTH_LONG).show()
}
}
}
}
}*/
}
private fun listItemClicked(subscriber: Subscriber) {
Toast.makeText(this, "${subscriber.name} is selected", Toast.LENGTH_SHORT).show()
viewModel.initUpdateAndDelete(subscriber)
}
}
You can convert a Flow type into a StateFlow by using stateIn method.
private val coroutineScope = CoroutineScope(Job())
private val flow: Flow<CustomType>
val stateFlow = flow.stateIn(scope = coroutineScope)
In order to transform the CustomType into UIState, you can use the transformLatest method on Flow. It will be something like below:
stateFlow.transformLatest { customType ->
customType.toUiState()
}
Where you can create an extension function to convert CustomType to UiState like this:
fun CustomType.toUiState() = UiState(
x = x,
y = y... and so on.
)
I am not sure what is exactly happening but when ApiService.apiService.getPokemon(name) in fun getPokemon in PokemonRepository.kt is called then the function getPokemon stops executing and emited livedata are then observed as null in DetailAktivity.kt instead of a valid Pokemon class.
I have checked the API call and it is working in other cases. I am new to Android programming, so I would appreciate some detailed explanation.
Here are the classes:
PokemonRepository.kt
class PokemonRepository(context: Context) {
companion object {
private val TAG = PokemonRepository::class.java.simpleName
}
private val pekemonDao = PokemonDatabase.getInstance(context).pokemonDao()
fun getPokemon(name: String) = liveData {
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
pekemonDao.getOne(name).map {
it
}
} catch (e: Exception) {
Log.e(TAG, "Getting data from the Internet failed", e)
pekemonDao.getOne(name).map {
e
}
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
companion object {
const val ITEM = "item"
}
private lateinit var binding: ActivityDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val vm: DetailViewModel by viewModels()
vm.pokemon.observe(
this,
{
binding.name.text = it.name
supportActionBar?.apply {
setDisplayShowTitleEnabled(true)
title = it.name
}
}
)
intent.extras?.apply {
vm.setCharacterId(getString(ITEM)!!)
}
}
}
DetailViewModel
class DetailViewModel(application: Application) : AndroidViewModel(application) {
private val repository = PokemonRepository(application)
private val name: MutableLiveData<String> = MutableLiveData()
val pokemon = name.switchMap { name ->
repository.getPokemon(name)
}
fun setCharacterId(characterId: String) {
name.value = characterId
}
}
ApiService.kt
interface ApiService {
#GET("pokemon?offset=0&limit=151")
suspend fun getPokemons(#Query("page") page: Int): NamedApiResourceList
#GET("pokemon/{name}")
suspend fun getPokemon(#Path("name") name: String): Pokemon
companion object {
private const val API_ENDPOINT = "https://pokeapi.co/api/v2/"
val apiService by lazy { create() }
private fun create(): ApiService = Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient())
.build()
.create(ApiService::class.java)
}
}
Pokemon data class
#Parcelize
#JsonClass(generateAdapter = true)
#Entity
data class Pokemon(
#PrimaryKey val id: Int,
val name: String,
#ColumnInfo(name = "base_experience") val baseExperience: Int,
val height: Int,
#ColumnInfo(name = "is_default") val isDefault: Boolean,
val order: Int,
val weight: Int,
val sprites: PokemonSprites,
) : Parcelable
PokemonDao.kt
#Dao
interface PokemonDao {
#Query("SELECT * FROM namedapiresource")
fun getAll(): LiveData<List<NamedApiResource>>
#Query("SELECT * FROM pokemon WHERE name=:name")
fun getOne(name: String): LiveData<Pokemon>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllNamedApiResources(vararg characters: NamedApiResource)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllPokemons(vararg characters: Pokemon)
}
I would guess because of how your getPokemon is defined:
val disposable = emitSource(
// Attempts to get a pokemon from the database - presumably this does
// not exist at first so would return null first
pekemonDao.getOne(name).map {
it
}
)
// AFTER NULL IS RETURNED this tries to fetch from the API
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
// After fetching from API, finally returns a non-null
pekemonDao.getOne(name).map {
it
}
So maybe just get ride of the initial block?
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
I want call hight order suspend function from other class with parameter and i don't know how.
class CharactersListViewModel : ViewModel() {
private val dataSourceFactory =
PageKeyDataSourceFactory(
scope = viewModelScope,
request = suspend {createRequest(0)
}
)
private suspend inline fun createRequest(offset : Int): MutableList<CharacterItem> {
val repository = Injection.provideMarvelRepository()
val response = repository.getCharacters(
offset = offset,
limit = PAGE_MAX_ELEMENTS
)
return CharacterItemMapper().map(response).toMutableList()
}
other class
class PageKeyDataSourceFactory<Value>(
private val scope: CoroutineScope,
private var request: suspend () -> MutableList<Value>
) : DataSource.Factory<Int, Value>() {
private var dataSource: PageKeyDataSource<Value>? = null
override fun create(): DataSource<Int, Value> {
dataSource = PageKeyDataSource(request = request, scope)
sourceLiveData.postValue(dataSource)
return dataSource as PageKeyDataSource<Value>
}
and class here i call function
in loadAfter function comes a params that I want to be used to call request.invoke()
class PageKeyDataSource<Value>(
private val request: suspend() -> MutableList<Value>,
private val scope: CoroutineScope,
) : PageKeyedDataSource<Int, Value>() {
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Value>) {
scope.launch(
CoroutineExceptionHandler { _, _ ->
retry = {
loadAfter(params, callback)
}
networkState.postValue(NetworkState.Error(true))
}
) {
val data = request.invoke()
callback.onResult(data, params.key + PAGE_MAX_ELEMENTS)
networkState.postValue(NetworkState.Success(true, data.isEmpty()))
}
}
}
class PageKeyDataSource<Value>(private val request: suspend (yourParams:YourType) -> MutableList<Value>) {
...
// some code
val list:MutableList<Value> = request.invoke(yourParams)
}
I can't find a better way to change listId of my VideosDataSource methods like load initial. I'm using view pager so it load 2 fragment at a time that's why i can't use getter/setter to set listId of my data source.
here my data source class:
class VideosDataSource(
private val networkService: NetworkService,
private val compositeDisposable: CompositeDisposable
): PageKeyedDataSource<String, Item>() {
var state: MutableLiveData<State> = MutableLiveData()
private var retryCompletable: Completable? = null
private var listId = "PL8fVUTBmJhHKEJjTNWn-ykf67rVrFWYtC"
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, Item>) {
updateState(State.LOADING)
compositeDisposable.add(
networkService.getPlaylistVideos(listId
,""
,Constants.API_KEY)
.subscribe( { response ->
updateState(State.DONE)
callback.onResult(response.items, response.prevPageToken, response.nextPageToken)
},
{
updateState(State.ERROR)
setRetry(Action { loadInitial(params,callback) })
}
)
)
}
here i'm trying to change listId in my view pager fragment.
my data source factory:
class VideosDataSourceFactory(
private val compositeDisposable: CompositeDisposable,
private val networkService: NetworkService
): DataSource.Factory<String, Item>() {
val videosDataSourceLiveData = MutableLiveData<VideosDataSource>()
override fun create(): DataSource<String, Item> {
val videosDataSource = VideosDataSource(networkService,
compositeDisposable)
videosDataSourceLiveData.postValue(videosDataSource)
return videosDataSource
}
}
my view model:
class PageViewModel(application: Application) :
AndroidViewModel(application) {
//paging
private val networkService = NetworkService.getService()
var videosList: LiveData<PagedList<Item>>
private val compositeDisposable = CompositeDisposable()
private val pageSize = 50
private val videosDataSourceFactory: VideosDataSourceFactory
init {
//paging
videosDataSourceFactory = VideosDataSourceFactory(compositeDisposable, networkService)
val config = PagedList.Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(pageSize)
.setEnablePlaceholders(false)
.build()
videosList = LivePagedListBuilder<String, Item>(videosDataSourceFactory, config).build()
}
In fragmnet onClick() i want to send listId to data source.
Whit some approaches like getter/setter i can be able to send listId to data source but view pager create two or three fragment at a time the value is override in getter/setter.
I'm looking for the better way to send data from fragment to data source.
I did it. The idea of constructor is good but not works for me because of init method of view model called before setting the id to factory constructor. But thanks to mr.pskink for his comments to use parameter constructor.
So here's how i did it.
In fragment i set list to view model.
companion object {
private const val ARG_SECOND = "arg_second"
#JvmStatic
fun newInstance(second: Array<Pair<String, String>>): PlaceholderFragment {
return PlaceholderFragment().apply {
arguments = Bundle().apply {
putString(ARG_SECOND,second[0].second)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
setListId(arguments?.getString(ARG_SECOND) ?: "")
}
}
In view model i create a method:
fun setListId(listId: String){
videosDataSourceFactory.listId = listId
}
In data source factory i craete a variable:
val videosDataSourceLiveData = MutableLiveData<VideosDataSource>()
lateinit var listId:String
override fun create(): DataSource<String, Item> {
val videosDataSource = VideosDataSource(networkService, compositeDisposable,listId)
videosDataSourceLiveData.postValue(videosDataSource)
return videosDataSource
}
And then get it via constructor of data source here:
class VideosDataSource(
private val networkService: NetworkService,
private val compositeDisposable: CompositeDisposable,
private val listId: String
): PageKeyedDataSource<String, Item>() {
var state: MutableLiveData<State> = MutableLiveData()
private var retryCompletable: Completable? = null
override fun loadInitial(params: LoadInitialParams<String>, callback:
LoadInitialCallback<String, Item>) {
updateState(State.LOADING)
compositeDisposable.add(
networkService.getPlaylistVideos(listId
,""
,Constants.API_KEY)
.subscribe( { response ->
updateState(State.DONE)
callback.onResult(response.items, response.prevPageToken, response.nextPageToken)
},
{
updateState(State.ERROR)
setRetry(Action { loadInitial(params,callback) })
}
)
)
}
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.