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>
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 am trying to practice the android architecture components Paging
Local + Remote Datasource with Room, MVVM and LiveData
When i first time scroll the list(get remote data), it get into loop by onItemAtEndLoaded in PagedList.BoundaryCallback, but it scroll smooth when open the activity next time (get local data)
Here is my github link here!
Can anyone take a look and help me how to fix it, Thanks!
Activity
class PagingActivity : AppCompatActivity() {
lateinit var viewModel: PagingViewModel
lateinit var adapter: PagingAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_paging)
val factory = PagingViewModelFactory(PagingRepository(), application)
viewModel = ViewModelProviders.of(this,factory).get(PagingViewModel::class.java)
adapter = PagingAdapter()
recyclerView.adapter = adapter
viewModel.pagedListLiveData.observe(this, Observer {
adapter.submitList(it)
})
}
}
ViewModel
class PagingViewModel(repository: PagingRepository, application: Application) :
AndroidViewModel(application) {
val pagedListLiveData = repository.getDataItem(application)
}
Repository
class PagingRepository : PagingRepositoryCallback {
private lateinit var localDataSource: DataSource.Factory<Int, DataItem>
override fun getDataItem(application: Application): LiveData<PagedList<DataItem>> {
val pagedListLiveData: LiveData<PagedList<DataItem>> by lazy {
localDataSource = DataItemDbHelper(application).getRoomDataItemDao().getAllDataItem()
val config = PagedList.Config.Builder()
.setPageSize(25)
.setEnablePlaceholders(false)
.build()
LivePagedListBuilder(localDataSource, config)
.setBoundaryCallback(PagingBoundaryCallback(application))
.build()
}
return pagedListLiveData
}
}
interface PagingRepositoryCallback {
fun getDataItem(application: Application): LiveData<PagedList<DataItem>>
}
BoundaryCallback
class PagingBoundaryCallback(context: Context) :
PagedList.BoundaryCallback<DataItem>() {
private var page = 2
private val api = AllPlayerApi.api
private val dao = DataItemDbHelper(context).getRoomDataItemDao()
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
api.getAllPlayer().enqueue(createWebserviceCallback())
}
override fun onItemAtEndLoaded(itemAtEnd: DataItem) {
super.onItemAtEndLoaded(itemAtEnd)
api.getAllPlayer(page).clone().enqueue(createWebserviceCallback())
}
private fun createWebserviceCallback(): Callback<AllPlayerData> {
return object : Callback<AllPlayerData> {
override fun onFailure(call: Call<AllPlayerData>?, t: Throwable?) {
Log.d("Huang", " get player fail ")
}
override fun onResponse(call: Call<AllPlayerData>?, response: Response<AllPlayerData>) {
Log.d("Huang", " onResponse " + page)
response.body()!!.data!!.forEach {
it.imageUrl = "https://pdc.princeton.edu/sites/pdc/files/events/new-nba-logo-1.png"
}
insertItemsIntoDb(response)
page++
}
}
}
private fun insertItemsIntoDb(response: Response<AllPlayerData>) {
GlobalScope.launch {
response.body()!!.data!!.forEach {
dao.insert(it)
}
}
}
}
Logic for, If onItemAtEndLoaded get the same itemAtEnd , then do nothing.
var lastItemAtEnd:DataItem? = null
override fun onItemAtEndLoaded(itemAtEnd: DataItem) {
lastItemAtEnd?.timestamp?.apply{
if(itemAtEnd.timestamp==this){
return;
}
}
super.onItemAtEndLoaded(itemAtEnd)
api.getAllPlayer(page).clone().enqueue(createWebserviceCallback())
}
As your page size is 25 so Pagelist config should have setInitialLoadSizeHint as 25 for avoiding looping/unnecessary call of onItemAtEndLoaded method
val config = PagedList.Config.Builder()
.setPageSize(25)
.setInitialLoadSizeHint(25) //same as your page size
.setEnablePlaceholders(false)
.build()
I know it's been long but i just post the solution in case someone need.
you should register an observer for your adapter and listen for onItemRangeInserted event and if the start position of item range is zero just simply scroll adapter to zero position, this make your RecyclerView on first load stay in zero position and by the way you should set setPrefetchDistance value smaller than the setInitialLoadSizeHint.
This is the Java code for adapters observer
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if(positionStart == 0)
recyclerView.scrollToPosition(positionStart);
}
});
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
The recycleView isn't updating the result from the network on initial loading.
RecycleView:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mRecyclerAdapter = MovieListAdapter(context)
rvMovieList.apply {
// Dedicated layouts for Screen Orientation
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
layoutManager = LinearLayoutManager(context)
} else {
layoutManager = GridLayoutManager(context, 2)
}
adapter = mRecyclerAdapter
}
}
and listening to the network result using LiveData from ViewModel.
LiveData listening snippet the Fragment below:
override fun onResume() {
super.onResume()
// Listen to data change
viewModel.getMovies().observe(this, mMovieListObserver)
}
private val mMovieListObserver: Observer<PagedList<MovieItem>> = Observer { movieItems ->
Log.d(TAG, "MovieItems: ${movieItems.size}")
showEmptyList(movieItems?.size == 0)
mRecyclerAdapter.submitList(movieItems)
}
private fun showEmptyList(isEmpty: Boolean) {
tvEmptyListView.visibility = if (isEmpty) View.VISIBLE else View.GONE
rvMovieList.visibility = if (isEmpty) View.GONE else View.VISIBLE
}
override fun onPause() {
viewModel.getMovies().removeObserver(mMovieListObserver)
super.onPause()
}
The irony is, the result populates the recycleView on subsequent loads. I feel the LiveData isn't working as expected. The expectation while introducing the emptyView was to show/hide the recycleView/EmptyView based on the result from the network.
ViewModel pasted below:
class MovieListViewModel : ViewModel() {
private val PAGE_SIZE = 10
internal var movies: LiveData<PagedList<MovieItem>>
init {
val dataSourceFactory = MovieDataSourceFactory()
val pagedListConfig = PagedList.Config.Builder()
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE)
.setEnablePlaceholders(true)
.build()
movies = LivePagedListBuilder(dataSourceFactory, pagedListConfig)
// .setBoundaryCallback() TODO
.build()
}
fun getMovies(): LiveData<PagedList<MovieItem>> {
return movies
}
}
Thanks for the time, appreciate any inputs to the solution or best practices. Thanks.
Repo: https://gitlab.com/faisalm/MovieDirect
////---
Updated the DataSourceFactory and DataSource.
class MovieDataSourceFactory : DataSource.Factory<Int, MovieItem>() {
private val mutableLiveData = MutableLiveData<MovieDataSource>()
override fun create(): DataSource<Int, MovieItem> {
val dataSource = MovieDataSource()
mutableLiveData.postValue(dataSource)
return dataSource
}
}
class MovieDataSource internal constructor() : PageKeyedDataSource<Int, MovieItem>() {
private val movieDbService: MovieDbService = RetrofitFactory.create()
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, MovieItem>) {
val moviesListCall = movieDbService.fetchLatestMoviesPaged(Constants.API_KEY, 1)
moviesListCall.enqueue(object : Callback<MoviesList> {
override fun onResponse(call: Call<MoviesList>, response: Response<MoviesList>) {
if (response.isSuccessful) {
val moviesLists = response.body()?.results
callback.onResult(moviesLists!!, 1, 2)
}
}
override fun onFailure(call: Call<MoviesList>, t: Throwable) {}
})
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, MovieItem>) {}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, MovieItem>) {
val moviesListCall = movieDbService.fetchLatestMoviesPaged(Constants.API_KEY, params.key)
moviesListCall.enqueue(object : Callback<MoviesList> {
override fun onResponse(call: Call<MoviesList>, response: Response<MoviesList>) {
if (response.isSuccessful) {
val moviesLists = response.body()?.results
callback.onResult(moviesLists!!, params.key + 1)
}
}
override fun onFailure(call: Call<MoviesList>, t: Throwable) {}
})
}
}
I think the issue is the way you're adding and removing the observer for the liveData.
Instead of adding in onResume and removing in onPause, just observe it in onActivityCreated in the Fragment. LiveData's observe method takes in a LifeCycleOwner (which is what you're passing with this in the Fragment), and it'll take care of making sure it's observing at the correct time in that lifecycle.
So remove these lines:
viewModel.getMovies().removeObserver(mMovieListObserver) viewModel.getMovies().addObserver(this, mMovieListObserver)
and add this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.getMovies().observe(this, Observer { movieItems ->
Log.d(TAG, "MovieItems: ${movieItems.size}")
showEmptyList(movieItems?.loadedCount == 0)
mRecyclerAdapter.submitList(movieItems)
})
}
How to determine size of data returned before setting adapter?
How to use emptyview with paging library?
How to set emptyview if pagedlist returns null or no data?
Update[24/04/19]:
I just found out that the library already provide us a way to listen to empty initial load, using PagedList.BoundaryCallback<YourItem>.
*Note that my old answer is still a valid alternative.
val livedPageList = LivePagedListBuilder(sourceFactory, config)
.setBoundaryCallback(object: PagedList.BoundaryCallback<YourItem>() {
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
// Handle empty initial load here
}
override fun onItemAtEndLoaded(itemAtEnd: YourItem) {
super.onItemAtEndLoaded(itemAtEnd)
// Here you can listen to last item on list
}
override fun onItemAtFrontLoaded(itemAtFront: YourItem) {
super.onItemAtFrontLoaded(itemAtFront)
// Here you can listen to first item on list
}
})
.build()
Original Answer:
Based on this class on google sample Network State. Modify it to handle empty content in initialLoad.
#Suppress("DataClassPrivateConstructor")
data class NetworkState private constructor(
val status: Status,
val msg: String? = null
) {
enum class Status {
RUNNING,
SUCCESS_LOADED, // New
SUCCESS_EMPTY, // New
FAILED
}
companion object {
val EMPTY = NetworkState(Status.SUCCESS_EMPTY) // New
val LOADED = NetworkState(Status.SUCCESS_LOADED) // New
val LOADING = NetworkState(Status.RUNNING)
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
}
}
Usage as follow:
class DataSource: PageKeyedDataSource<Long, Item>() {
val initialLoad: MutableLiveData<NetworkState> = MutableLiveData()
override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<Long, Item>) {
initialLoad.postValue(NetworkState.LOADING)
apiCallSource.subscribe({ items ->
if (items.isEmpty()) {
initialLoad.postValue(NetworkState.EMPTY)
} else {
initialLoad.postValue(NetworkState.LOADED)
}
}, { error ->
// handle error
})
}
}
And this is how the activity handle it:
class activity: AppCompatActivity() {
val viewModel = // init viewmodel
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.refreshState.observe(this, Observer { networkState ->
if (it == NetworkState.LOADING) {
// Show loading
} else {
// Hide loading
if (it.status == NetworkState.Status.SUCCESS_EMPTY) {
// Show empty state for initial load
}
}
}
}
}
For more details on how to connect DataSource with Activity, see this sample
Simply add a listener or callback function to your DataSourceFactory and your DataSource and call it if the list in loadInitial is empty:
class DataSourceFactory(
private val dataObservable: Observable<List<Data>>,
private val onEmptyAction: () -> Unit
) : DataSource.Factory<Int, Data >() {
override fun create(): DataSource {
return DataSource(observable, onEmptyAction)
}
}
class DataSource(
private val observable: Observable<List<Data>>,
private val onEmptyAction: () -> Unit
) : ItemKeyedDataSource<Int, Data>() {
private val data = mutableListOf<Data>()
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Data>) {
observable
.subscribe({ data ->
if (data.isEmpty()) {
// Inform someone that this list is empty from the
// beginning to be able to show an empty page
onEmptyAction()
}
// rest of your code & logic
}, { Timber.e(it) })
}
}
In your fragment/activity you are observing network state:
viewModel.getNetworkState1()?.observe(this, Observer {
// here you can handle you empty view
setEmptyView()
})
like this:
private fun setNoTransactionsLayout() {
if(viewModel.listIsEmpty()) {
yourTextView.visibility = View.VISIBLE
} else {
yourTextView.visibility = View.GONE
}
}
And in view model you have this function:
fun listIsEmpty(): Boolean {
return yourPagedList?.value?.isEmpty() ?: true
}