Paging Library doesn't trigger observe when load initial data - android

I'm setting up paged list of items that i fetch in datasource with coroutines, also i'm observing this list to submit it in adapter, but when it initially loads some data, it does not trigger observe callback. What can i do to handle this ?
I tried to debug this thing, and i found that ArrayList> mCallbacks list in PagedList, does not contain any callbacks when it tried to notify data changes, but i don't know what to do with this.
Data source from data will be fetched and paged.
class PagedDataSource(private val account: Account, private val getItems: GetItems): PageKeyedDataSource<Int, Item>() {
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Transaction>
) {
GlobalScope.launch {
val startPage = 0
account.id?.let {
val items = getItems(it, startPage).body.toMutableList()
callback.onResult(items, null, 1)
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
GlobalScope.launch {
account.id?.let {
val list = getItems(it, params.key)
val items = list.body.toMutableList()
callback.onResult(items, if (params.key >= list.totalPages) null else params.key + 1)
}
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
//NO NEED
}
}
code in fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagedAdapter = PagedAdapter()
vItems.layoutManager = LinearLayoutManager(context)
vItems.isNestedScrollingEnabled = false
vItems.adapter = pagedAdapter
items.observe(viewLifecycleOwner, Observer { items ->
if (items != null && items.isNotEmpty()) {
pagedAdapter.submitList(items)
} else {
vItemsTitle.visibility = View.VISIBLE
}
})
}
And finally code in my viewmodel
init {
items = initializedPagedList()
}
private fun initializedPagedList() : LiveData<PagedList<Item>> {
val factory = object: DataSource.Factory<Int, Item>() {
override fun create(): DataSource<Int, Item> {
return PagedDataSource(account, getItems)
}
}
val config = PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.build()
return LivePagedListBuilder(factory, config).build()
}
I expect that data will be fetched after success api calls in loadInitial method and trigger observe callback.

Finally after researching, i found answer for my question.
Problem was in this code
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagedAdapter = PagedAdapter()
vItems.layoutManager = LinearLayoutManager(context)
vItems.isNestedScrollingEnabled = false
vItems.adapter = pagedAdapter
items.observe(viewLifecycleOwner, Observer { items ->
if (items != null && items.isNotEmpty()) {
pagedAdapter.submitList(items)
} else {
vItemsTitle.visibility = View.VISIBLE
}
})
}
I needed to change this to
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagedAdapter = PagedAdapter()
vItems.layoutManager = LinearLayoutManager(context)
vItems.isNestedScrollingEnabled = false
vItems.adapter = pagedAdapter
items.observe(viewLifecycleOwner, Observer { items ->
pagedAdapter.submitList(items)
})
}
I think that's happening because in foreground PagedList works asynchronously/ and you need to submit this list once and after that datasource will send updates directly to the adapter, avoiding observing. In my case i'm submiting if list is not empty, but it will always empty when you're creating PagedList at the start.
Good luck everyone !

Related

Firestore with StateFlow real time change is observed in repo and view model, but adapter is not being updated

As a developer one needs to adapt to change, I read somewhere it says:
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
I wanted to implement Clean Architecture with MVVM
My app data flow will look like this:
Model class
data class Note(
val title: String? = null,
val timestamp: String? = null
)
Dtos
data class NoteRequest(
val title: String? = null,
val timestamp: String? = null
)
and
data class NoteResponse(
val id: String? = null,
val title: String? = null,
val timestamp: String? = null
)
Repository layer is
interface INoteRepository {
fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteRepositoryImpl is:
class NoteRepositoryImpl: INoteRepository {
private val mFirebaseFirestore = Firebase.firestore
private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)
private val noteList = mutableListOf<NoteResponse>()
private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null
override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
getNoteListSuccessListener = success
}
override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
deleteNoteSuccessListener = success
}
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
override fun deleteNoteById(noteId: String) {
mNotesCollectionReference.document(noteId)
.delete()
.addOnSuccessListener {
deleteNoteSuccessListener?.invoke(noteList)
}
}
}
ViewModel layer is:
interface INoteViewModel {
val noteListStateFlow: StateFlow<List<NoteResponse>>
val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteViewModelImpl is:
class NoteViewModelImpl: ViewModel(), INoteViewModel {
private val mNoteRepository: INoteRepository = NoteRepositoryImpl()
private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteListStateFlow: StateFlow<List<NoteResponse>>
get() = _noteListStateFlow.asStateFlow()
private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
get() = _noteDeletedStateFlow.asStateFlow()
init {
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener {
viewModelScope
.launch {
_noteListStateFlow.emit(it)
Log.e("NOTE_G_VM", "$it")
}
}
// deleteNoteSuccessListener
mNoteRepository
.deleteNoteSuccessListener {
viewModelScope
.launch {
_noteDeletedStateFlow.emit(it)
Log.e("NOTE_D_VM", "$it")
}
}
}
override fun getNoteList() {
// Get all notes
mNoteRepository.getNoteList()
}
override fun deleteNoteById(noteId: String) {
mNoteRepository.deleteNoteById(noteId = noteId)
}
}
and last but not least Fragment is:
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
private lateinit var adapter: NoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerViewNotes
recyclerView.addOnScrollListener(
ExFABScrollListener(binding.fab)
)
adapter = NoteAdapter{itemView, noteId ->
if (noteId != null) {
showMenu(itemView, noteId)
}
}
recyclerView.adapter = adapter
// initView()
fetchFirestoreData()
binding.fab.setOnClickListener {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(action)
}
}
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
//In the showMenu function from the previous example:
#SuppressLint("RestrictedApi")
private fun showMenu(v: View, noteId: String) {
val menuBuilder = MenuBuilder(requireContext())
SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
menuBuilder.setCallback(object : MenuBuilder.Callback {
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
return when(item.itemId){
R.id.option_edit -> {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
findNavController().navigate(action)
true
}
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
true
} else -> false
}
}
override fun onMenuModeChange(menu: MenuBuilder) {}
})
val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
menuHelper.show()
}
}
With all the above logic I'm facing TWO issues
issue - 1
As mentioned here, I have added SnapshotListener on collection as:
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.
issue - 2
If I delete any item from list/recyclerview using:
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.
Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.
Make following changes:
In init{} block of NoteViewModelImpl :
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener{noteResponseList ->
viewModelScope.launch{
_noteListStateFlow.emit(it.toList())
}
}
you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Impose StateFlow
lifecycleScope.launch {
viewModel.noteListStateFlow.collect { list ->
adapter.submitList(list.toMutableList())
}
}
}
That's it, I hope it works fine.
Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:
// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method
private fun fetchFirestoreData() {
...
// remove this line
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
adapter.submitList(data)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
And the same for the delete menu item (R.id.option_delete).

Android Recycleview DiffUtil doesn't work properly

I am using recycle view with diffutil in my application. but while I rotating or comeback from another screen the adapter gets updated. why is this happening?.
Here My ViewModel
class FeedsViewModel() : ViewModel() {
private val feedsRepository = FeedsRepository()
val feedsLiveData: MutableLiveData<Resource<UserFeeds>> = MutableLiveData()
init {
val apiParams = HashMap<String, String>()
apiParams["user_id"] = "1"
getFeeds(apiParams,"123"
}
fun getFeeds(apiParams: HashMap<String, String>, token: String) = viewModelScope.launch {
feedsLiveData.postValue(Resource.Loading())
val response = feedsRepository.getFeeds(apiParams, token)
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
feedsLiveData.postValue(Resource.Success(resultResponse))
}
} else {
feedsLiveData.postValue(Resource.Error(response.message()))
}
}
}
I am using fragment to display it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerViewFeeds.adapter = feedsAdapter
viewModel.feedsLiveData.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
binding.progressBar.visibility = View.GONE
response.data?.let { userFeeds ->
feedsAdapter.differ.submitList(userFeeds.userPosts.toList())
binding.nooFeeds.visibility = View.GONE
}
is Resource.Error -> {....}
is Resource.Loading -> {....}
}
})
}
and my adapter
class FeedsAdapter(private val context: Context, private val itemClickListener: FeedsItemCallBack) :
RecyclerView.Adapter<FeedsAdapter.MyViewHolder>() {
class MyViewHolder(val bindin: ItemViewFeedsBinding) : RecyclerView.ViewHolder(bindin.root) {
}
private val differCallback = object : DiffUtil.ItemCallback<UserPost>() {
override fun areItemsTheSame(oldItem: UserPost, newItem: UserPost): Boolean {
return oldItem.postId == newItem.postId
}
override fun areContentsTheSame(oldItem: UserPost, newItem: UserPost): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
var feedsItem = differ.currentList[position]
holder.bindin.feedData = feedsItem;
holder.bindin.executePendingBindings()
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
Is this implementation is correct?.
Is this issue of ViewModel or adapter?
please help. Thanks in advance
you could distinguish the cases of your activity being created for
the first time and being restored from savedInstanceState. This is
done by overriding onSaveInstanceState and checking the parameter of
onCreate.
You could lock the activity in one orientation by adding
android:screenOrientation="portrait" (or "landscape") to
in your manifest.
You could tell the system that you meant to handle screen changes
for yourself by specifying
android:configChanges="orientation|screenSize" in the
tag. This way the activity will not be recreated, but will receive a
callback instead (which you can ignore as it's not useful for you).
Personally, I'd go with (3). Of course if locking the app to one of the orientations is fine with you, you can also go with (2).

notifyDataSetChanged() is not working after fetching data with Retrofit and DisposableSingleObserver

I've been trying to solve this problem for over 3 hours. Everything seems just fine on the Logcat and Debug mode. I'm fetching the List without any problem, Fragment is reading the MutableLiveData successfully. Only the notifyDataSetChanged() function is not working and also it doesn't give any error etc. If I send an ArrayList manually then it works but if it goes inside Retrofit and DisposableSingleObserver then even the manual list doesn't work.
I have tried every way that I could have found on the internet. I've looked for more than 20 different solution none of them have worked.
API - HoroscopeAPI.kt
interface HoroscopeAPI {
#GET("astraios/horoscopeList.json")
fun getHoroscope(): Single<List<Horoscope>>
}
Service - HoroscopeAPIService.kt
class HoroscopeAPIService {
private val BASE_URL = "https://wiuma.co"
private val api = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
.create(HoroscopeAPI::class.java)
fun getData(): Single<List<Horoscope>> {
return api.getHoroscope()
}
}
ViewModel - HoroscopeViewModel.kt
class HoroscopeViewModel : ViewModel() {
private val horoscopeApiService = HoroscopeAPIService()
private val disposable = CompositeDisposable()
val horoscopeList = MutableLiveData<List<Horoscope>>()
fun getDataFromAPI() {
loadingStatus.value = true
disposable.add(
horoscopeApiService.getData()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<List<Horoscope>>() {
override fun onSuccess(t: List<Horoscope>) {
horoscopeList.value = t
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
})
)
}
}
Fragment - Horoscope.kt
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
horoscopeViewModel =
ViewModelProvider(this).get(HoroscopeViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_horoscope, container, false)
horoscopeViewModel.getDataFromAPI()
horoscopeRecyclerView = root.findViewById(R.id.horoscopeRecyclerView)
horoscopeRecyclerView.layoutManager = LinearLayoutManager(context)
horoscopeRecyclerView.adapter = recyclerViewAdapter
observeData()
return root
}
fun observeData() {
horoscopeViewModel.horoscopeList.observe(viewLifecycleOwner, Observer { horoscope ->
horoscope?.let {
recyclerViewAdapter.updateList(horoscope)
}
})}
**Adapter - HoroscopeRecyclerAdapter.kt **
class HoroscopeRecyclerAdapter(val horoscopeList: ArrayList<Horoscope>) :
RecyclerView.Adapter<HoroscopeRecyclerAdapter.HoroscopeViewHolder>() {
class HoroscopeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HoroscopeViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.horoscope_recycler_row, parent, false)
return HoroscopeViewHolder(view)
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: HoroscopeViewHolder, position: Int) {
holder.itemView.horoscopeName.text = horoscopeList.get(position).nameHoroscope
holder.itemView.horoscopeDates.text =
horoscopeList.get(position).startDate + " " + horoscopeList.get(position).endDate
//TODO Gorsel baglantisi eklenecek.
holder.itemView.setOnClickListener {
val action =
HoroscopeFragmentDirections.actionNavigationHoroscopeToNavigationHoroscopeDetails(0)
Navigation.findNavController(it).navigate(action)
}
}
override fun getItemCount(): Int {
return horoscopeList.size
}
fun updateList(newHoroscopeList: List<Horoscope>) {
horoscopeList.clear()
horoscopeList.addAll(newHoroscopeList)
notifyDataSetChanged()
}}
I ran your project from Github. notifyDataSetChanged() seems to be working fine. The reason why the list items aren't showing up is that the visibility of the RecyclerView is set to GONE. It needs to be set back to VISIBLE when the results arrive:
fun observeData() {
horoscopeViewModel.horoscopeList.observe(viewLifecycleOwner, Observer { horoscope ->
horoscope?.let {
errorText.visibility = View.GONE
progressBar.visibility = View.GONE
horoscopeRecyclerView.visibility = View.VISIBLE
recyclerViewAdapter.updateList(it)
}
})

Android Room with LiveData + ViewModel Refresh Question

I have a small app I am using to try learn more about some of the newer Android components. I'm finding it difficult to find information and understand how best to do what I want.
Currently: Open app -> load data + stores in DB -> display data in list
I want to be able to query data again upon button press.
I have 2 buttons, 1 to fetch data again, 1 to delete the list data from the DB.
Problem is that it seems you cannot refresh if you are observing on an instance of LiveData, which I am. I understand that however the way I found to actually do a Network call and store in the Database returns an instance of LiveData and I am not sure how best to proceed.
Let me show you the code.
Fragment
private val viewModel: quoteViewModel by viewModels()
private lateinit var binding: FragmentHomeBinding
private lateinit var adapter: QuoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerView()
setupRetrieveQuotesObserver()
setupDeleteDataListener()
setupFetchNewDataListener()
setupSwipeToRefresh()
}
private fun initRecyclerView() {
adapter = QuoteAdapter()
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun setupDeleteDataListener() {
binding.removeQuotesButton.setOnClickListener {
viewModel.removeAllQuotes()
}
}
private fun setupFetchNewDataListener() {
binding.getQuotesButton.setOnClickListener {
viewModel.removeQuotes()
viewModel.getQuotes()
}
}
private fun setupRetrieveQuotesObserver() {
viewModel.quoteLiveDataList.observe(viewLifecycleOwner, Observer { result ->
when (result.status) {
NewResult.Status.SUCCESS -> {
result.data.let { adapter.setItems(ArrayList(result.data)) }
binding.progressBar.visibility = View.GONE
binding.swipeContainer.isRefreshing = false
}
NewResult.Status.ERROR -> {
binding.progressBar.visibility = View.GONE
Snackbar.make(binding.root, "Some error has occurred", Snackbar.LENGTH_SHORT)
.show()
}
NewResult.Status.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
}
}
})
}
private fun setupSwipeToRefresh() {
binding.swipeContainer.setOnRefreshListener {
viewModel.getQuotes()
}
}
ViewModel
val quoteLiveDataList: LiveData<NewResult<List<Quote>>> = repository.quotes
fun getQuotes() = viewModelScope.launch {
repository.quotes
}
fun removeAllQuotes() = viewModelScope.launch {
repository.deleteAllQuotes()
}
Repository
val quotes = performGetOperation(
databaseQuery = { dao.getAllQuotes() },
networkCall = { remoteSource.getAllQuotes() },
saveCallResult = {
val quotesList = ArrayList<Quote>()
for (messageString in it.messages.non_personalized) {
quotesList.add(
Quote(
messageString,
FaceImageProvider().getRandomFacePicture(),
false
)
)
}
dao.insertQuotes(quotesList)
}
)
#WorkerThread
suspend fun deleteAllQuotes() = withContext(Dispatchers.IO) { dao.deleteAllQuotes() }
performGetOperation
This is a class I saw online for handling what I want to do. I think the issue stems from here as it is returning LiveData, I'm not sure how best to fix it
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> NewResult<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<NewResult<T>> =
liveData(Dispatchers.IO) {
emit(NewResult.loading())
val source = databaseQuery.invoke().map { NewResult.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == NewResult.Status.SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == NewResult.Status.ERROR) {
emit(NewResult.error(responseStatus.message!!))
emitSource(source)
}
}
RemoteDataSource
suspend fun getQuotes() = getResult { service.getQuotes() }
getResult
protected suspend fun <T> getResult(call: suspend () -> Response<T>): NewResult<T> {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
return NewResult.success(body)
}
}
return error("${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): NewResult<T> {
Log.d("BaseDataSource", message)
return NewResult.error("Network called failed due to: $message")
}
NewResult
data class NewResult<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING,
}
companion object {
fun <T> success(data: T): NewResult<T> {
return NewResult(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): NewResult<T> {
return NewResult(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): NewResult<T> {
return NewResult(Status.LOADING, data, null)
}
}
Apologies for the very long message, but I guess I need to show all the little bits and bobs I'm using.
I think the problem is in the Fragment where I do viewModel.quoteLiveDataList.observe, as it is returning a new LiveData if it is called again. So I'm not sure how I can do another server call and update the DB and return it here.
Appreciate any help!
Thanks
Use Transformations.switchMap on a MutableLiveData to trigger your repository call like it is done here in the GithubBrowserSample project. This will allow you to implement the refresh functionality -
private val _getQuotes = MutableLiveData<Boolean>()
val quotes: LiveData<NewResult<List<Quote>>> = _getQuotes.switchMap { getQuotes ->
repository.quotes
}
fun getQuotes() {
_getQuotes.value = true
}
fun refresh() {
_getQuotes.value?.let {
_getQuotes.value = it
}
}

LiveData is not updating the View consistently

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)
})
}

Categories

Resources