java.lang.IllegalStateException: Cannot invoke observeForever on a background thread
Asked Answered
D

5

20

Can someone help me find where I am going wrong here. I need to continously observer network data and update the UI whenever there is a data change from the Worker. Please note that this was working before upgrading to androidx.

Here is a Worker class.

class TestWorker(val context: Context, val params: WorkerParameters): Worker(context, params){

    override fun doWork(): Result {
        Log.d(TAG, "doWork called")
        val networkDataSource = Injector.provideNetworkDataSource(context)
        networkDataSource.fetchData(false)

        return Worker.Result.SUCCESS
    }

    companion object {
        private const val TAG = "MY_WORKER"
    }

}

Which is called as follows:

fun scheduleRecurringFetchDataSync() {
    Log.d("FETCH_SCHEDULER", "Scheduling started")

    val fetchWork = PeriodicWorkRequest.Builder(TestWorker::class.java, 1, TimeUnit.MINUTES)
            .setConstraints(constraints())
            .build()
    WorkManager.getInstance().enqueue(fetchWork)
}

private fun constraints(): Constraints{
    return Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
}

I also have a UserDao and UserRepository to fetch and store data. I am observing the network data in the UserRepository as follows:

class UserRepository (
    private val userDao: UserDao,
    private val networkDataSource: NetworkDataSource,
    private val appExecutors: AppExecutors){

init {
    val networkData= networkDataSource.downloadedData
    networkData.observeForever { newData->
        appExecutors.diskIO().execute {
            userDao.insert(newData.user)
        }
    }}

Can someone help me locate where I am going wrong. This is giving me error as follows:

java.lang.IllegalStateException: Cannot invoke observeForever on a background thread
    at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:443)
    at androidx.lifecycle.LiveData.observeForever(LiveData.java:204)
    at com.example.app.data.repo.UserRepository.<init>(UserRepository.kt:17)
    at com.example.app.data.repo.UserRepository$Companion.getInstance(UserRepository.kt:79)
Doerrer answered 3/10, 2018 at 8:41 Comment(3)
To use LiveData in a test setup requires the core-testing library and InstantTaskExecutorRule - see: #49840944Huntingdon
Not sure if I am getting you correctly, but I am not doing any tests. Besides where should I add the code exactly to get rid of this error. I just added the testing library but the error is still thereDoerrer
My mistake - I thought TestWorker was a test component. In that case your problem looks to be that networkData.observeForever is invoked off the main thread. If UserRepository has to be created on a different thread you will need to marshal this call to run on the main thread. An easy way to do this would be to use a Handler.Huntingdon
A
32

Change this:

networkData.observeForever { newData->
    appExecutors.diskIO().execute {
        userDao.insert(newData.user)
    }
}

To:

Variant B (with coroutines):

GlobalScope.launch(Dispatchers.Main) { networkData.observerForever { /*..*/ } }

But be aware, the usage of GlobalScope is not recommended: https://mcmap.net/q/334195/-why-not-use-globalscope-launch

Variant A (without coroutines):

Handler(Looper.getMainLooper()).post { networkData.observeForever{ /*..*/ } }

Explanation

Normally observe(..) and observeForever(..) should be called from the main thread because their callbacks (Observer<T>.onChanged(T t)) often change the UI which is only possible in the main thread. That's the reason why android checks if the call of the observe functions is done by the main thread.

In your case UserRepository.init{} is called by a background thread, so the exception is thrown. To switch back to the main thread you can use one of the above variants. But be aware the code inside of your observe callback is executed by the main thread, too. Any expensive processing inside this callback will freeze your UI!

Archlute answered 8/1, 2019 at 11:8 Comment(6)
Actually he could just use executors.mainThread() instead of Handler(Looper.getMainLooper()).Vickers
You are right, there are some other possibilities to switch to the main thread. If he is using kotlin coroutines he can switch by using GlobalScope.launch(Dispatchers.Main) { }, too :)Archlute
This wont work you cant make network calls on the main thread. So basically we can no longer use LiveData to access network or the local DB??? This does not make sense.Lennalennard
@JasonCrosby look closely, the network call is not done on the main thread, only the observation of the result is done on the main thread ;) but writing this result into a DB on the main thread, like in his example, is a code smell.Archlute
Dispatchers.Main tells the coroutine to run on the main thread.Lennalennard
Again, only the observing part (networkData.observerForever { /*..*/ }) runs on the main thread. The part before runs on a background thread. Otherwise, he would have not got the exception ;)Archlute
P
15

In another solution, you can call it from main dispatcher as

GlobalScope.launch(Dispatchers.Main) {
  // your code here...
}
Pillbox answered 27/9, 2019 at 9:29 Comment(0)
S
2

In my case I was testing liveData and I forgot to add the InstantTaskExecutorRule().

    @RunWith(AndroidJUnit4::class)
    class UserDaoTest {
        @get:Rule // <----
        var instantExecutorRule = InstantTaskExecutorRule() // <----
        ....
    }

Don't forget to add the library to the project.

    testImplementation"androidx.arch.core:core-testing:2.1.0" // unit tests
    androidTestImplementation "androidx.arch.core:core-testing:2.1.0"//instrumentation tests
Sometime answered 15/3, 2022 at 21:35 Comment(0)
C
1

Additionally to the nice and detailled answer from @user1185087 here is a solution if you're using RxJava in your project. It's maybe not that short, but if you already use RxJava in your project, it's an elegant way to switch to the required thread (in this case the Android's UI thread via .observeOn(AndroidSchedulers.mainThread())).

Observable.just(workManager.getStatusById(workRequest.getId()))
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(status -> status.observeForever(workStatus -> {
        // Handling result on UI thread
    }), err -> Log.e(TAG, err.getMessage(), err));
Carmen answered 16/4, 2019 at 7:58 Comment(0)
L
0
  • Here is what I did in my Java code to make it work
// the LiveData query
LiveData<List<Calf>> calfLiveDataList = getCalfDao().getAllCalves();

Handler handler = new Handler(Looper.getMainLooper()); //This is the main thread
       handler.post(new Runnable() { //task to run on main thread
                        @Override
                        public void run() {
                            calfLiveDataList.observeForever(observer);
                        }
                    }
       );

  • Ignore my naming conventions, my app is for cow farmers.
Lynn answered 26/10, 2021 at 18:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.