I have a simple app that displays contact list pulled from an API.
I need to display the latest contacts fetch if there is no network at the launch of the app. So that's why I'm working with Room to save the contacts into a database.
The contacts are correctly saved, they are correctly pulled when needed.
But there is a weird problem when I do the following pattern:
- I pull some contacts from the API (auto-saved into the local db)
- I kill the app;
- I cut all network to trigger the pull from the local db;
- I launch the app without any network, contacts are correctly displayed from the local db;
- I open the network to process a fresh call to the API (clean the db and so on...)
- After the response to the call to the API, after the subscribe of the getContacts
call, the subscribe of the getContactsFromDatabase is called !!
So after debugging I found that just the subscribe is called and not the full function getContactsFromDatabase()
because of my breakpoint on the srList.isRefreshing = true
is not triggered. Only the breakpoint in the subscribe part.
I also tried to put a breakpoint to the only part where the getContactsFromDatabase function is called. It's never triggered however the breakpoint in the subscribe is triggered.
You can check my code on Github
ContactListFragment.kt:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dataStorage = DataStorage(requireContext())
initView()
initListeners()
}
private fun initView() {
val layoutManager = LinearLayoutManager(activity)
rvContact.layoutManager = layoutManager
rvContact.itemAnimator = DefaultItemAnimator()
adapter = ContactListAdapter(this::onContactClicked)
rvContact.adapter = adapter
getContacts()
}
private fun initListeners() {
srList.setOnRefreshListener {
viewModel.page = 1; viewModel.contacts.clear(); getContacts()
}
rvContact.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val manager = rvContact.layoutManager as LinearLayoutManager
val visibleItemCount: Int = manager.childCount
val totalItemCount: Int = manager.itemCount
val firstVisibleItemPosition: Int = manager.findFirstVisibleItemPosition()
if (!srList.isRefreshing) {
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
&& firstVisibleItemPosition >= 0
&& totalItemCount >= ContactListViewModel.PAGE_SIZE
) {
getContacts()
}
}
}
})
}
private fun getContacts() {
srList.isRefreshing = true
disposable.add(viewModel.getContactByPages()
.doOnError { e ->
srList.isRefreshing = false
if (viewModel.launch){
Timber.e("get contacts database")//breakpoint not triggered
getContactsFromDatabase()
}
e.localizedMessage?.let {
Timber.e(it)
}
val message = when (e) {
is BadRequestException -> {
getString(R.string.common_error_bad_request)
}
is ServerErrorException -> {
getString(R.string.common_error_server_error)
}
is UnknownHostException -> {
getString(R.string.common_error_no_connection)
}
else -> {
getString(R.string.common_error_basic)
}
}
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
.subscribe({ result ->
srList.isRefreshing = false
viewModel.page++
if (dataStorage.getBoolean(IS_FROM_CACHE)){//need a variable to clean the database after a first successful fetch
dataStorage.putBoolean(IS_FROM_CACHE, false).subscribe()
viewModel.contacts.clear()
cleanContactListOfDatabase()
}
viewModel.contacts.addAll(result.contacts)
saveContactsToDatabase()
adapter.updateList(viewModel.contacts)
tvNumberOfResult.text = getString(
R.string.contact_list_fragment_number_of_result,
viewModel.contacts.size
)
}, Throwable::printStackTrace)
)
}
private fun getContactsFromDatabase() {
srList.isRefreshing = true//breakpoint not triggered here
disposable.add(viewModel.getContactFromDatabase()
.doOnError {
srList.isRefreshing = false
}
.subscribe({
srList.isRefreshing = false// breakpoint triggered here
viewModel.launch = false
viewModel.contacts.addAll(it)
adapter.updateList(viewModel.contacts)
tvNumberOfResult.text = getString(
R.string.contact_list_fragment_number_of_result,
viewModel.contacts.size
)
dataStorage.putBoolean(IS_FROM_CACHE, true).subscribe()
}, Throwable::printStackTrace)
)
}
private fun saveContactsToDatabase() {
disposable.add(viewModel.insertAllContactsToDataBase()
.doOnError {
Timber.e("Insert error")
}
.subscribe({
Timber.d("Contacts saved")
}, Throwable::printStackTrace)
)
}
private fun cleanContactListOfDatabase(){
disposable.add(viewModel.cleanContactList()
.doOnError {
Timber.e("clean table error")
}
.subscribe({
Timber.e("Table cleaned")
}, Throwable::printStackTrace)
)
}
To resume the problem, the subscribe method of the viewModel.getContactFromDatabase()
is triggered without the call of the function getContactsFromDatabase()
.
Open the app without any network (there are contacts displayed from the local db);
Open any network (wifi or 4g);
Try a swipe refresh to pull contacts from the API;
The subscribe of the getContacts()
is triggered (normal);
The subscribe of the viewModel.getContactFromDatabase()
is triggered without even the call of the function getContactsFromDatabase()
-- PROBLEM
You can check my code on Github
Following these docs:
room/accessing-data#query-rxjava
Flowable/Observable
Every time the user data is updated, the Flowable object will emit automatically, allowing you to update the UI based on the latest data.
In your code, after getContactByPages()
you call saveContactsToDatabase()
to save the data to database
So viewModel.getContactFromDatabase()
(which is point to ContactDao.getContacts()
) will emit data again.
If you want ContactDao.getContacts()
to emit only one time when is called, consider to convert ContactDao.getContacts()
to MayBe
/Single
instead of Observable
Hope this helps.