I am trying to achieve paging in my app using android paging library.But I am stuck in one place. The dataSource is not getting created from the factory. Snippet below.
private fun getLivePagedListBuilder(queryString: String): LivePagedListBuilder<Int, News> {
val dataSourceFactory = object : DataSource.Factory<Int, News>() {
override fun create(): DataSource<Int, News> {
return NewsDataSource(queryString)
}
}
return LivePagedListBuilder(dataSourceFactory, config)
}
1. The create method is not getting called. So the return inside tht method not firing.
My DataSource
class NewsDataSource(val searchQuery: String) : PageKeyedDataSource<Int, News>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, News>) {
api.searchNews(searchQuery, Constants.perPageLimit, 1)
.enqueue(object : Callback<NewsResponse> {
override fun onFailure(call: Call<NewsResponse>, t: Throwable) {
Log.d("TAG1", "Failure")
}
override fun onResponse(call: Call<NewsResponse>, response: Response<NewsResponse>) {
callback.onResult(response.body()?.news, response.body()?.page - 1, response.body()?.page + 1)
}
})
}
}
The API returns the page number and I am planning to load the next page when the scroll ends
What determines the datatype of the Key in DataSource.Factory<Int, News>()
I am really stuck on this :(
Related
I want to make my network request synchronous because the input of second request comes from the output of first request.
override fun onCreate(savedInstanceState: Bundle?) {
retrofit1 =Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/").addConverterFactory(GsonConverterFactory.create()).build()
retrofit2 =Retrofit.Builder()
.baseUrl("https://samples.openweathermap.org/").addConverterFactory(GsonConverterFactory.create()).build()
button.setOnClickListener { view ->
CoroutineScope(IO).launch {
fakeApiRequest()
}}
In my fakeApiRequest(),I am making two network request.
private suspend fun fakeApiRequest() {
val result1 :Geo?= getResult1FromApi()
val result2: Long? = getResult2FromApi(result1)}
Since,this is an asynchronous call,I am getting Null Pointer Exception in my getResult2FromApi(result1) method because the argument passed is null.
In order to fix this issue,I had to add delay(1500) in first call.
private suspend fun getResult1FromApi(): Geo? {
val service:CallService = retrofit1!!.create(CallService::class.java)
val call = service.getUsers()
call.enqueue(object : Callback<List<User>> {
override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
g = users.get(0).address.geo
}
override fun onFailure(call: Call<List<User>>, t: Throwable) {
}
})
delay(1500)
return g
}
-----------------------------------------------------------
private suspend fun getResult2FromApi(result1: Geo?): Long? {
val service2:CallService = retrofit2!!.create(CallService::class.java)
val call2 = service2.getWeather(result1?.lat!!, result1.lng,"b6907d289e10d714a6e88b30761fae22")
call2.enqueue(object : Callback<WeatherData> {
override fun onResponse(call: Call<WeatherData>, response: Response<WeatherData>) {
}
override fun onFailure(call: Call<WeatherData>, t: Throwable) {
}
})
return dt
}
Is there anyway I can make this synchronous, so that I don't have to pass any delay time.
You haven't implemented the suspendable function correctly. You must use suspendCoroutine:
suspend fun getResult1FromApi(): Geo? = suspendCoroutine { continuation ->
val service = retrofit1!!.create(CallService::class.java)
service.getUsers().enqueue(object : Callback<List<User>> {
override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
continuation.resume(response.result.getOrNull(0)?.address?.geo)
}
override fun onFailure(call: Call<List<User>>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
Now your function is synchronous and returns a Geo object.
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'm using Paging Library from JetPack for infinite scrolling. Everything is working fine, but I have more API addresses(URLs).
And what I want to do is for my fragments to change the URL but I don't know what function should I create to fetch each URL for the respective fragment.
I've tried a function to fetch each URL based on which Fragment is opened with a SortType class
Function to fetch:
private fun fetchPhotos(page : Int): Call<List<Photo>> {
if (sortType != null) {
return dataSource.fetchPhotos(sortType = sortType, page = page)
} else if (query.isNotEmpty()) {
return dataSource.fetchPhotos(page = page, query = query)
}
throw RuntimeException("Unknown state to fetch movies")
}
Here is PhotoDataSource where is the Pagination and where pages are fetched and API URLs:
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Photo>) {
networkState.postValue(NetworkState.LOADING)
initialLoad.postValue(NetworkState.LOADING)
fetchPhotos.getPhotos(FIRST_PAGE_NUMBER, params.requestedLoadSize).enqueue(object : Callback<List<Photo>>{
override fun onFailure(call: Call<List<Photo>>, t: Throwable) {
// keep a Completable for future retry
setRetry(Action { loadInitial(params, callback) })
val error = NetworkState.error(t.message)
// publish the error
networkState.postValue(error)
initialLoad.postValue(error) }
override fun onResponse(call: Call<List<Photo>>, response: Response<List<Photo>>) {
if (response.body() != null) {
setRetry(null)
networkState.postValue(NetworkState.LOADED)
initialLoad.postValue(NetworkState.LOADED)
callback.onResult(response.body()!!, null, FIRST_PAGE_NUMBER + INCREMENT_PAGE_VALUE)
}
}
})
}
API Services:
#GET("photos")
fun getPhotos(#Query("page") page: Int, #Query("per_page") per_page: Int): Call<List<Photo>>
#GET("photos/curated")
fun getCuratedPhotos(#Query("page") page: Int, #Query("per_page") per_page: Int): Call<List<Photo>>
#GET("/collections/featured")
fun getFeaturedCollections(#Query("page") page: Int, #Query("per_page") per_page: Int): Call<List<Photo>>
#GET("search/photos")
fun searchPhotos(#Query("page") page: Int, #Query("query") query: String): Call<List<Photo>>
SortType class:
enum class SortType(val value: Int) {
MOST_POPULAR(0),
HIGHEST_RATED(1),
UPCOMING(2)
}
I get no errors and nothing happens. I just want a way to make this more productive and I want to avoid creating a DataSource class for each API URL.
Hope you understand, I really need your help I've been struggling with this error for a few months and no idea what should I do. For any better explanations just ask.
i try to implement the pagination library , using rxJava , first of all , i call the NetworkApi to load the full data , then i want to use the pagiantion with the full loaded data, how can i do it with the library , i am trying to use the ItemKeyedDataSource Class , but please , do i need always to pass the element size to my api call or i can work only with the in memory loaded data ?
this is my api call :
public fun getMembersPagination(): MutableLiveData<ResultContainer<PagedList<MembersModel.Data.Member?>>> {
disposable = client.getMembersPaged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(0)
.subscribe(
{ result ->
onRetrieveUserData(result)
},
{ error -> onRetrieveUserDataError(error) }
)
return pagedList
}
i don't treat the pagiantion from my Api
this is the api :
#GET("members")
fun getMembersPaged(): Observable<PagedList<MembersModel.Data.Member?>>
ItemKeyedDataSource code :
class MembersPaginationDataSource(private val memberId: Int)
: ItemKeyedDataSource<Int, MembersModel.Data.Member?>() {
val client by lazy {
RetrofitClient.RetrofitClient()
}
var disposable: Disposable? = null
private var allMembers = MutableLiveData<PagedList<MembersModel.Data.Member?>>()
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<MembersModel.Data.Member?>) {
getMembersPagination().observe()
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<MembersModel.Data.Member?>) {
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<MembersModel.Data.Member?>) {
}
override fun getKey(item: MembersModel.Data.Member): Int = item.id!!
public fun getMembersPagination(): MutableLiveData<PagedList<MembersModel.Data.Member?>> {
disposable = client.getMembersPaged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(0)
.subscribe(
{ result ->
onRetrieveUserData(result)
},
{ error -> onRetrieveUserDataError(error) }
)
return allMembers
}
private fun onRetrieveUserData(membersModel: PagedList<MembersModel.Data.Member?>?) {
allMembers.postValue(membersModel)
}
private fun onRetrieveUserDataError(error: Throwable) {
allMembers.postValue(null)
}
}
i stop at that point
Function returns null before data.value is set in asynchronous onResponse().
How to make it first fetch data and then return that data?
fun getNews(code: String): LiveData<List<News>>{
val call = service.getNewsByCountry(code, Constant.API_KEY)
var data = MutableLiveData<List<News>>()
call.enqueue(object : Callback<NewsResponse> {
override fun onFailure(call: Call<NewsResponse>?, t: Throwable?) {
Log.v("retrofit", "call failed")
}
override fun onResponse(call: Call<NewsResponse>?, response: Response<NewsResponse>?) {
data.value = response!!.body()!!.articles
}
})
return data
}
You're making an asynchronous call, so data.value will not be set until that asynchronous call resolves. However, since you are generating a MutableLiveData, you should be able to observe, which will give you an update when your asynchronous call sets the value.
Just use object:Callback
accessTocken.enqueue(object : Callback<AccessToken> {
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
})
Try
fun getNews(code: String): LiveData<List<News>>{
val call = service.getNewsByCountry(code, Constant.API_KEY)
var data = MutableLiveData<List<News>>()
doAsync {
call.enqueue(object : Callback<NewsResponse> {
override fun onFailure(call: Call<NewsResponse>?, t: Throwable?) {
Log.v("retrofit", "call failed")
}
override fun onResponse(call: Call<NewsResponse>?, response: Response<NewsResponse>?) {
data.value = response!!.body()!!.articles
}
})
}
return data
}
If not exists doAsync try add follow anko dependency on your app/build.gralde
implementation "org.jetbrains.anko:anko-design:0.10.5"
Here I found a more extensive answer to your question on this article.
Before retrofit 2.6.0 you have to call enqueue() and implement callbacks. Now it is noτ necessary anymore.
You should change from this:
class TodoRepository {
var client: Webservice = RetrofitClient.webservice
fun getTodo(id: Int): LiveData<Todo> {
val liveData = MutableLiveData<Todo>()
client.getTodo(id).enqueue(object: Callback<Todo>{
override fun onResponse(call: Call<Todo>, response: Response<Todo>) {
if (response.isSuccessful) {
// When data is available, populate LiveData
liveData.value = response.body()
}
}
override fun onFailure(call: Call<Todo>, t: Throwable) {
t.printStackTrace()
}
})
// Synchronously return LiveData
// Its value will be available onResponse
return liveData
}
}
to this:
class TodoRepository {
var client: Webservice = RetrofitClient.webservice
suspend fun getTodo(id: Int) = client.getTodo(id)
}
Here you have the complete answer -> https://proandroiddev.com/suspend-what-youre-doing-retrofit-has-now-coroutines-support-c65bd09ba067