Search code examples
androidkotlinmvvmandroid-roomrx-java2

Android RxJava subscribe triggered from nowhere


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


Solution

  • Following these docs:

    room/accessing-data#query-rxjava

    room-rxjava-acb0cd4f3757

    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.