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.
Related
I have a ShoppingList App that is built on MVVM architecture Android.
I did not make it, but I followed a tutorial on Youtube.
This is the image of the app(1/2) where the shopping list is shown. The bottom right side is a button for adding new elements of the list.
This is the second view(2/2) where the dialog appears to enter a name of our element and amount of it. Here we have cancel button and add button.
The problem is when I click the ADD button on the Dialog Box I do not know how to get an ID of this added item to the recycler view on my VIEW and to make it appear via the TOAST command on my main Activity.
The question is - How to get an ID of a new added element to my shopping list and show it on my MainActivity(ShoppingActivity) VIEW when I click the ADD button?
If you need additional information ask me out immediately! I will provide you anything you need.
Code is provided here:
ShoppingActivity(View)
class ShoppingActivity : AppCompatActivity(), KodeinAware {
override val kodein by kodein()
private val factory: ShoppingViewModelFactory by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shopping)
// View Model is being created out of other classes to set changes to View
val viewModel = ViewModelProviders.of(this, factory).get(ShoppingViewModel::class.java)
// Adapters and Recycler View
val adapter = ShoppingItemAdapter(listOf(), viewModel)
rvShoppingItems.layoutManager = LinearLayoutManager(this)
rvShoppingItems.adapter = adapter
// ViewModel makes changes to the Activity
viewModel.getAllShoppingItems().observe(this, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
fab.setOnClickListener {
AddShoppingItemDialog(this ,
object: AddDialogListener{
override fun onAddButtonClicked(item: ShoppingItem) {
viewModel.upsert(item)
showToast(viewModel.getID(item).toString().toInt())
}
}).show()
}
}
fun showToast(id: Int) {
Toast.makeText(this#ShoppingActivity, "ID записи: $id", Toast.LENGTH_LONG).show()
}}
ShoppingViewModel(ViewModel)
class ShoppingViewModel(private val repository: ShoppingRepository): ViewModel() {
fun upsert(item: ShoppingItem) = CoroutineScope(Dispatchers.IO).launch {
repository.upsert(item)
}
fun delete(item: ShoppingItem) = CoroutineScope(Dispatchers.IO).launch {
repository.delete( item)
}
fun getID(item: ShoppingItem) = repository.getID(item)
fun getAllShoppingItems() = repository.getAllShoppingItems()
}
AddShoppingItemDialog(the logic of showing Dialog info)
class AddShoppingItemDialog(context: Context, var addDialogListener: AddDialogListener): AppCompatDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_add_shopping_item)
tvAdd.setOnClickListener {
val name = etName.text.toString()
val amount = etAmount.text.toString()
if(name.isEmpty()) {
Toast.makeText(context, "Please enter the name", Toast.LENGTH_LONG).show()
return#setOnClickListener
}
if(amount.isEmpty()) {
Toast.makeText(context, "Please enter the amount", Toast.LENGTH_LONG).show()
return#setOnClickListener
}
val item = ShoppingItem(name, amount.toInt())
// We need to
addDialogListener.onAddButtonClicked(item)
dismiss()
}
tvCancel.setOnClickListener {
cancel()
}
}}
Repository
class ShoppingRepository(private val db: ShoppingDatabase) {
suspend fun upsert(item: ShoppingItem) = db.getShoppingDao().upsert(item)
suspend fun delete(item: ShoppingItem) = db.getShoppingDao().delete(item)
fun getID(item: ShoppingItem) = db.getShoppingDao().getID(item)
fun getAllShoppingItems() = db.getShoppingDao().getAllShoppingItems()}
ShoppingDAO
#Dao
interface ShoppingDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(item: ShoppingItem) : Long
#Delete
suspend fun delete(item: ShoppingItem)
#Query("SELECT * FROM shopping_items WHERE id = $CURRENT_POSITION")
fun getID(item: ShoppingItem): LiveData<Int>
#Query("SELECT * FROM shopping_items")
fun getAllShoppingItems(): LiveData<List<ShoppingItem>>
}
ShoppingItem
const val CURRENT_POSITION = 0
#Entity(tableName = "shopping_items")
data class ShoppingItem(
#ColumnInfo(name = "item_name")
var name: String,
#ColumnInfo(name = "item_amount")
var amount: Int
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = CURRENT_POSITION
}
AddDialogListener
interface AddDialogListener {
fun onAddButtonClicked(item: ShoppingItem)
}
App View with added items
Since insert/upsert operations on Database are suspend functions, observe returned id in view model
In ShoppingViewModel
private var _itemId : Long = MutableLiveData<Long>()
val itemId : LiveData<Long>
get() = _itemId
fun upsert(item: ShoppingItem) = CoroutineScope(Dispatchers.IO).launch {
val id = repository.upsert(item)
_itemId.postValue(id)
}
In ShoppingActivity,
viewModel.itemId.observe(this, Observer {id ->
showToast(id)
})
Please let me know if you need to know more details.
You need to use SingleLiveEvent to observe the value only once.
Add this SingleLiveEvent class
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveEvent"
}
}
In ShoppingViewModel
Replace this
private var _itemId : Long = MutableLiveData<Long>()
val itemId : LiveData<Long>
get() = _itemId
with
private var _itemId : Long = SingleLiveEvent<Long>()
val itemId : LiveData<Long>
get() = _itemId
You can use Event wrapper when there are more than one observer.
For more details:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
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).
I can't observe the LiveData<PagedList> change in activity, but the UI is updated(the list has grown in activty).
I can only observe it once when the livedata is initialized.
when the paging library call loadAfter method, the ui is updated, but didn't call pageList.observe{}
Firstly, I put the process of data request into the Kotlin Coroutines, I can't observe the data change, then I used asynchronous requests instead.It still didn't work.
Here is my code:
PlayActivity main code
private val commentAdapter =
object : BasePagedAdapter(diffCallback, this) {
// just bind recycleview item and corresponding view model. etc.
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_play)
binding.vm = vm
binding.lifecycleOwner = this
val workId = intent.getLongExtra(WORK_ID, 0)
vm.listComment(workId)
play_rv_comment.adapter = commentAdapter
/* herer is the problem*/
vm.commentList.observe(this, Observer {
/*only log once when called loadInitial*/
LogUtils.e("observe", it)
commentAdapter.submitList(it)
})
PlayViewModel
class PlayViewModel : BaseViewModel() {
var workId: Long = 0
// the data which I want to observe
lateinit var commentList: LiveData<PagedList<WorkComment>>
private val commentPageSize = 15
fun listComment(workId: Long) {
// init by DataSource.Factory in android paging library
commentList = BaseDataSourceFactory(workId).toLiveData(commentPageSize)
}
DataSource.Factory in Android paging
class BaseDataSourceFactory(
val workId: Long
) :
DataSource.Factory<Long, WorkComment>() {
override fun create(): DataSource<Long, WorkComment> {
return object : PageKeyedDataSource<Long, WorkComment>() {
override fun loadInitial(
params: LoadInitialParams<Long>,
callback: LoadInitialCallback<Long, WorkComment>
) {
try {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
null, 2)
}
})
} catch (e: SocketTimeoutException) {
ToastUtils.showShort("请稍候重试")
} catch (e: Exception) {
LogUtils.e(e.localizedMessage)
}
}
// called many times, but I can't observe the PagedList change
override fun loadAfter(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
params.key + 1
)
}
})
}
override fun loadBefore(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
}
}
}
}
Retrofit Api
interface WorkCommentApi {
/**
* list comment
*/
#GET("public/work/comment")
fun listComment(#Query("workId") workId: Long, #Query("current") current: Long, #Query("size") size: Int): Call<TResult>
}
I want to know what should I do to observe the LiveData<PagedList> change
This is happening because each time you call vm.listComment(workId), the object you first bound in activity is killed and new object is created.
You can use Transformations with MediatorLiveData.
Activity:
viewModel.logout().observe(this, Observer {
// do here
})
ViewModel:
class RepackViewModel(app: Application) : BaseViewModel(app) {
// IMPORTANT - Mediator
val logout = MediatorLiveData<PagedList<WorkComment>>()
fun logout() : LiveData<PagedList<WorkComment>> = logout
init {
// IMPORTANT - passes repo update to activity
logout.addSource(repo.getLogoutResponse()) { logout.postValue(it) }
}
}
Repository:
class BaseRepository(val app: Application) {
private val logout = MutableLiveData<PagedList<WorkComment>>()
fun getLogoutResponse(): LiveData<PagedList<WorkComment>> = logout
override fun create(): DataSource<Long, WorkComment> {
//when you get your data
logout.value = // your value
}
You need to have your work id be mutable data to be observed by the transformation. so whenever you update your work, id, it will fetch comments. Like Thus...
ViewModel:
val workIdMutableLiveData: MutableLiveData<Int> = MutableLiveData(workId)
//This performs the meat of the work to display the items in the recyclerview
var commentsList = Transformations.switchMap(workIdMutableLiveData) { workId ->
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(pagingLimit)
.build()
val pagedListBuilder = LivePagedListBuilder<Long, WorkComment>(BaseDataSourceFactory(workId), config)
pagedListBuilder.build()
}
Then in your activity, observe
yourViewModel.commentsList.observe(this, Observer { list ->
list ?: return#Observer
adapter.submitList(list)
yourRecyclerView.adapter = adapter
})
Whenever you update the workIdMutableLiveData by doing a
workIdMutableLiveData.postValue(workId)
...The recyclerview will update. Your recyclerview must inherit from PagedListAdapter.
After testing, I knew list couldn't be observed when it has inner data change, like add(), remove(). etc.
It only be observed when its reference has been changed, like create or assignment operation:
list.value = null
So I couldn't observe the data change of LiveData<List>
I have 70 itens stored on my ROM and I would like to fetch a paged amount of 15. I read many posts so far with related issues, however none of them were useful for me.
Some possible causes for loadAfter not being triggered:
Solution 1 : call getItem inside onBindViewHolder
Solution 2 : call submitList to PagedListAdapter
Solution 3 : replace ListAdapter with PagedListAdapter
I assume DataBinding is fine since everything works without trying to paging.
I'm mocking my data source to understand what's happening. Some functions are suspended 'cause they should have data coming from ROM which requires it. My code state be like:
ADAPTER
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
getItem(position).let { wkda ->
with(holder) {
wkda?.apply { bind(createOnClickListener(this)) }
}
}
}
FRAGMENT
vm.manufacturers.observe(viewLifecycleOwner) { manufacturers ->
adapter.submitList(manufacturers)
}
VIEWMODEL
var manufacturers: MutableLiveData<PagedList<WKDA>> = MutableLiveData()
init {
viewModelScope.launch {
repository.getManufacturers(manufacturers)
}
}
REPOSITORY
suspend fun getManufacturers(manufacturers: MutableLiveData<PagedList<WKDA>>) {
withContext(Dispatchers.IO) {
manufacturers.postValue(ManufacturerPagedList.
getInstance().
fetchPage())
}
}
MANUFACTURER PAGED LIST
private val executor = ManufacturerExecutor()
private val paginationConfig: PagedList.Config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setPrefetchDistance(FETCH_DISTANCE)
.setEnablePlaceholders(false)
.build()
companion object {
#Volatile
private var instance: ManufacturerPagedList? = null
fun getInstance() = instance ?: synchronized(this) {
ManufacturerPagedList().also {
instance = it
}
}
}
fun fetchPage(): PagedList<WKDA> = PagedList.Builder<Int, WKDA>(
MockDataSource(),
paginationConfig)
.setInitialKey(INITIAL_KEY)
.setFetchExecutor(executor)
.setNotifyExecutor(executor)
.build()
}
DATASOURCE
class MockDataSource : PageKeyedDataSource<Int, WKDA>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.requestedLoadSize) }.toList(), -1, 1)
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.key) }.toList(), params.key + 1)
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.key) }.toList(), params.key - 1)
}
private fun generatePost(key: Int): WKDA {
return WKDA("name", "author $key")
}
}
CONSTANTS
const val INITIAL_KEY: Int = 0
const val PAGE_SIZE: Int = 15
const val FETCH_DISTANCE: Int = 1
What am I missing here?
After check: loadAfter was called properly. The problem was model itself:
wkda.id had always the same "name" value
DiffCallback compared old list of objects with the new one and didn't see differences, so the item "duplicates" weren't added to the adapter
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) })
}
)
)
}