I'm using Paging Library with PagedListAdapter, I'm using BoundaryCallback in PagedList to know when the user reaches the end of the list. The problem is that the method in my callback never gets called.
This is my data source code:
class PropertyDataSource : ItemKeyedDataSource<Int, Property>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Property>) {
callback.onResult(getProperties(1, params.requestedLoadSize))
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Property>) {
Thread.sleep(1000)
callback.onResult(getProperties(params.key + 1, params.requestedLoadSize + params.key))
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Property>) {
}
override fun getKey(item: Property): Int {
return item.id
}
}
my code for boundary callback is
class MyBoundryCallBack: PagedList.BoundaryCallback<Property>() {
override fun onItemAtEndLoaded(itemAtEnd: Property) {
Log.e("alz", "at end $itemAtEnd")
}
override fun onItemAtFrontLoaded(itemAtFront: Property) {
Log.e("alz", "at front $itemAtFront")
}
override fun onZeroItemsLoaded() {
Log.e("alz", "zero item loaded")
}
}
here is my activity code
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = MyListAdapter(PropertyDiffCallback())
mainRecyclerView.adapter = adapter
mainRecyclerView.layoutManager = LinearLayoutManager(this)
val propertyDataSourceFactory = PropertyDataSourceFactory()
val config = PagedList.Config.Builder()
.setPageSize(2)
.setInitialLoadSizeHint(3)
.setEnablePlaceholders(false)
.build()
val pagedList = LivePagedListBuilder(propertyDataSourceFactory, config)
.setBoundaryCallback(MyBoundryCallBack())
.build()
pagedList.observe(this, Observer {
adapter.submitList(it)
}
}
And my code for Adapter is look like this
class MyListAdapter(diffCallback: DiffUtil.ItemCallback<Property>) :
PagedListAdapter<Property, RecyclerView.ViewHolder>(diffCallback) {
class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val titleTV: TextView = itemView.findViewById(R.id.title)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_property, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(getItem(position) == null) return
val itemHolder = holder as ItemHolder
itemHolder.titleTV.text = getItem(position)!!.title
}
}
And here is my implementation of getProperties() that will return some mock data. Later on, I'm going to change it to load data from the server.
fun getProperties(from: Int, to: Int): List<Property> {
val list = ArrayList<Property>()
for (i in from..to){
list.add(Property(i, "item $i"))
}
return list
}
Note that, my code works fine and my adapter requesting more data as it goes to the end, I just don't get the callback when PagedList gets to the end.
Related
I'm using the JetPack paging library with a network call (no database).
I am able to scroll down smoothly and load new pages of data, BUT, when scrolling up it stutters and quickly jumps to the top of the list. I am unable to scroll up smoothly.
Here is a video showing the problem: https://imgur.com/a/bRoelyF
What I've Tried:
Enabling retrofit caching
Using a LinearLayoutManager instead of GridLayoutManager
Following old and newer tutorials with versions 1.0.1 and 2.1.2 of the library
Here is my code:
MovieDataSource.kt:
private val movieDbApi: TheMovieDbApi
) : PageKeyedDataSource<Int, Movie>() {
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {}
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Movie>) {
movieDbApi.getTopRatedMovies(BuildConfig.MOVIE_DATA_BASE_API, FIRST_PAGE).subscribe(
{
it?.let { callback.onResult(it.results, null, FIRST_PAGE + 1) }
}, {}
)
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
movieDbApi.getTopRatedMovies(BuildConfig.MOVIE_DATA_BASE_API, params.key).subscribe(
{
val key = params.key + 1
it?.let {callback.onResult(it.results, key)
}
},{}
)
}
MovieDataSourceFactory.kt:
class MovieDataSourceFactory(private val movieDbApi: TheMovieDbApi) :
DataSource.Factory<Int, Movie>() {
// Is this where the MovieDataSource callBacks are sent?
val movieLiveDataSource = MutableLiveData<MovieDataSource>()
override fun create(): DataSource<Int, Movie> {
val movieDataSource = MovieDataSource(movieDbApi)
movieLiveDataSource.postValue(movieDataSource)
return movieDataSource
}
}
HomeViewModel.kt:
class HomeViewModel #Inject constructor(
theMovieDbApi: TheMovieDbApi
) : DisposingViewModel() {
var moviePagedList: LiveData<PagedList<Movie>>
private var liveDataSource: LiveData<MovieDataSource>
init {
val movieDataSourceFactory = MovieDataSourceFactory(theMovieDbApi)
liveDataSource = movieDataSourceFactory.movieLiveDataSource
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(MovieDataSource.PAGE_SIZE)
.build()
moviePagedList = LivePagedListBuilder(movieDataSourceFactory, config)
.build()
}
}
HomeViewModel.kt:
class HomeActivity : AppCompatActivity() {
#Inject
internal lateinit var viewModelFactory: ViewModelFactory<HomeViewModel>
private lateinit var viewModel: HomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
AndroidInjection.inject(this)
val adapter = HomeAdapter()
movie_recycler_view.setHasFixedSize(false)
movie_recycler_view.layoutManager = LinearLayoutManager(this)
val viewModel = ViewModelProvider(this, viewModelFactory).get(HomeViewModel::class.java)
viewModel.moviePagedList.observe(this, Observer {
adapter.submitList(it)
})
movie_recycler_view.adapter = adapter
}
}
HomeAdapter.kt:
class HomeAdapter : PagedListAdapter<Movie, HomeAdapter.MovieViewHolder>(USER_COMPARATOR) {
override fun getItemCount(): Int {
return super.getItemCount()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_movie, parent, false)
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val movie = getItem(position)
movie?.let { holder.bind(it) }
}
class MovieViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(movie: Movie) {
Picasso.get().load(BASE_IMAGE_URL + movie.poster_path).into(itemView.movie_image)
}
}
companion object {
private val USER_COMPARATOR = object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem == newItem
}
}
}
If anyone has a solution or spots a problem I'd love to hear it!
I solved the problem.
It's because I didn't add a placeholder image to Picasso in the adapter.
Before:
Picasso.get()
.load(BASE_IMAGE_URL + movie.poster_path)
.into(itemView.movie_image)
After:
Picasso.get()
.load(BASE_IMAGE_URL + movie.poster_path)
.placeholder(R.drawable.placeholder)
.into(itemView.movie_image)
Now it loads well.
Another consideration is the size of the image, it takes a while to load a larger image especially if you are loading many of them within an infinite scroll.
I am doing a school project.
I have a list with Doses, so I need to fetch data en set text one by one.
Right now I'm getting:
kotlin.UninitializedPropertyAccessException: lateinit property medicine has not been initialized.
So I need to wait till the first item is fetched and set before continuing to next item.
can you help me?
class ClientDoseListAdapter(private val doses: List<Dose>) : RecyclerView.Adapter<ClientDoseListAdapter.ViewHolder>() {
private lateinit var medicine : Medicine
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.client_dose_listitem, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = doses[position]
runBlocking {
displayMedicine(item.medicine)
}
holder.med_name.text = medicine.name
holder.dose_amount.text = item.amount.toString()
}
private suspend fun displayMedicine(id: Int) {
fetchMedicine(id)
}
override fun getItemCount(): Int = doses.size
inner class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
LayoutContainer
private fun fetchMedicine(id: Int) {
service.getMedicine(id, "Bearer ${ClienOverzichtFragment.auth}")
.enqueue(object : Callback<List<Medicine>> {
override fun onResponse(call: Call<List<Medicine>>, response: Response<List<Medicine>>) {
if (response.code() == 200) {
val temp = response.body()!!
medicine = temp[0]
Log.v("SHIT", medicine.name)
} else {
Log.v("SHIT", response.code().toString())
//TODO
}
}
override fun onFailure(call: Call<List<Medicine>>, t: Throwable) {
Log.v("SHIT", "FAILED : "+t.message)
}
})
}
}
Move your service call out of the Recycler (best into a ViewModel, but can call from Activity or using any other pattern - the main thing, shouldn't be part of the Recycler) and pass the data, when it's received, into the Recycler.
Your ClientDoseListAdapter to accept medicine:
class ClientDoseListAdapter(private val doses: List<Dose>, private val medicine: Medicine)
In your activity, initiate and a call for medicine and observe it - when the data arrives, pass it to the adapter. Assuming you use a view model your code in Activity would look something like this:
viewModel.getMedicine().observe(
this,
Observer<Medicine> { medicine ->
//assuming doses come from somewhere else
adapter = ClientDoseListAdapter(doses, medicine, this)
clientDoseRecyclerView.adapter = adapter
}
)
Here is my first time to apply MVVM concept to my Android Application. I follow the steps at the referenced article
https://medium.com/swlh/realtime-firestore-pagination-on-android-with-mvvm-b5e30cea437
And I am managed to load data successfully. When it comes to implementing the onclick event at the row of my RecyclerView List, it comes out that there has no onlick response .
Would you please suggest the better method to implement the onCLick method given that I have applied PageListAdapter?
When I study the PageListAdapter documentation on Android, it seems no clue for me to implement the onclick method.
class MovieViewModel(movieRepository: MovieRepository) : ViewModel() {
private val viewModelJob = SupervisorJob()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
var selected: MutableLiveData<RealtimeMovie>? = null
private val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.setPageSize(20)
.build()
val records: LiveData<PagedList<RealtimeMovie>> =
LivePagedListBuilder<String, RealtimeMovie>(
MovieDataSource.Factory(movieRepository, uiScope),
config
).build()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here is my adapter:
class MovieAdapter : PagedListAdapter<RealtimeMovie, MovieAdapter.MovieViewHolder>(
object : DiffUtil.ItemCallback<RealtimeMovie>() {
override fun areItemsTheSame(old: RealtimeMovie, new: RealtimeMovie): Boolean =
old.id == new.id
override fun areContentsTheSame(old: RealtimeMovie, new: RealtimeMovie): Boolean =
old == new
}
) {
private lateinit var onItemClick: (movie: RealtimeMovie) -> Unit
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.list_movie,
parent,
false
)
return MovieViewHolder(view)
}
infix fun setOnItemClick(onClick: (movie: RealtimeMovie) -> Unit) {
this.onItemClick = onClick
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val record = getItem(position)
holder.bind(record)
holder.itemView.setOnClickListener { onItemClick(record!!) }
}
override fun onViewRecycled(holder: MovieViewHolder) {
super.onViewRecycled(holder)
holder.apply {
txtRecordName.text = ""
crdRecord.isEnabled = true
crdRecord.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
android.R.color.white
)
)
viewHolderDisposables.clear()
}
}
inner class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val viewHolderDisposables = CompositeDisposable()
val crdRecord by lazy { view.findViewById<MaterialCardView>(R.id.crd_record) }
val txtRecordName by lazy { view.findViewById<TextView>(R.id.txt_record_name) }
fun bind(RealtimeMovie: RealtimeMovie?) {
RealtimeMovie?.let {
it.record
.subscribeBy(
onNext = {
txtRecordName.text = it.title
},
onError = {
// Handle error here
// Record maybe deleted
}
)
.addTo(viewHolderDisposables)
}
}
}
}
Here is my fragment :
..
viewModel = ViewModelProviders.of(this, factory).get(MovieViewModel::class.java)
viewModel.records.observe(this, Observer {
swpRecords.isRefreshing = false
recordsAdapter.submitList(it)
recordsAdapter.setOnItemClick {
print("movie : ${it.id}" )
print("movie : ${it.record}" )
}
})
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)
})
}
I want to fetch some json data, see in the image the green arrow:
The problem is that Android Studio doesn't let me get the data I want. It stops until a step before (I think). In my adapter class check:
holder?.view?.textWeather?.text = weatherFor.weather.toString()
Also it shows me in the emulator the red arrow, what is this?
Below is my main Activity's json method with the classes i want to fetch data for, and the associated Adapter class.
Main Activity
fun fetchJson() {
val url="https://api.openweathermap.org/data/2.5/forecast?q=Prague,CZ&appid=4cf7f6610d941a1ca7583f50e7e41ba3"
val request=Request.Builder().url(url).build()
val client= OkHttpClient()
client.newCall(request).enqueue(object :Callback {
override fun onResponse(call: Call?, response: Response?) {
val body=response?.body()?.string()
println(body)
val gson=GsonBuilder().create()
val forecastfeed=gson.fromJson(body,ForecastFeed::class.java)
runOnUiThread{
recyclerView_main.adapter=MainAdapter(forecastfeed)
}
}
override fun onFailure(call: Call?, e: IOException?) {
println("Failed to execute request")
}
})
}
class ForecastFeed(val list:List<ForecastWeatherList>) { }
class ForecastWeatherList(val weather:List<WeatherData>) { }
class WeatherData(val main:String,val icon:String) { }
Adapter
class MainAdapter(val forecastfeed: ForecastFeed): RecyclerView.Adapter<CustomViewHolder>() {
val forecastWeather = listOf<String>("First","Second")
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val weatherFor = forecastfeed.list.get(position)
holder?.view?.textWeather?.text = weatherFor.weather.toString()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder{
//how do we even create a view
val layoutInflater =LayoutInflater.from(parent?.context)
val cellForRow=layoutInflater.inflate(R.layout.weather_row,parent,false)
return CustomViewHolder(cellForRow)
}
override fun getItemCount(): Int {
return forecastfeed.list.count()
}
}
class CustomViewHolder(val view: View):RecyclerView.ViewHolder(view) { }
You can format the data manually
holder?.view?.textWeather?.text = "weather ${weatherFor.weather.map{it.main}.joinToString(", ")}"
or use data classes
You need to overwrite WeatherData.toString() to have a hand on what's displayed.
class WeatherData(val main:String,val icon:String) {
override fun toString(): String {
return "$main $icon"
}
}
Further more you should use a RecyclerView with a ViewHolder to handle properties one-by-one and enable more complex layouts. If needed.