2

I'm starting a new thread from my activity, this thread does a 10 second operation and then reports back to the UI with runOnUiThread()

During the 10 second operation, the UI becomes unresponsive and does not respond to any user interaction. In this case I am attempting to close the activity using a button in the toolbar. An ANR error is thrown but the button click is processed after the worker thread has finished.

Although, while the thread is working, the app is still able to display a spinning ProgressBar which wouldn't happen if the work was being done on the UI thread.

The Profiler shows that the UI thread is sleeping during this work, so to my understanding it should be responsive?. I've tried using AsyncTask instead but that also doesn't work. Anyway here is some code:

The new Thread is started when the window comes into focus:

Activity:

  @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        if(hasFocus && !recyclerSetup){
            progressBar.setIndeterminate(true);
            progressBar.setVisibility(View.VISIBLE);
            WorkThread thread = new WorkThread();
            thread.start();
        }
    }

Thread:

  private class WorkThread extends Thread {

        @Override
        public void run() {
            getViewModelAndWords();
            runOnUiThread(() -> setupRecycler());
        }
    }

  private void getViewModelAndWords() {
        viewModel = ViewModelProviders.of(this).get(WordViewModel.class);
        adapter = new WordDetailedAdapter(this, viewModel, this, this, !favGroup.equals(ANY_WORD_PARAM));
        allWords = viewModel.getAllWords();
    }

I'm not sure if the viewModel has anything to do with the issue or not, but it's the viewModel.getAllWords() method which performs a heavy 10 second Room db operation.

Here's a snapshot of the Profiler showing the sleeping UI thread and worker Thread (AsyncTask #6):

enter image description here

EDIT:

Okay, so I think the issue lies within the room DB operation / viewModel. Replacing the contents of getAllWords() with Thread.sleep(10000); free'd up the UI thread for user interaction, therefore it's the following code which is (for some reason) preventing user input:

EDIT 2:

As suggested, I now use onPostExecute() along with an interface to retrieve the words:

 public static class GetAllWordsWithCallBackTask extends AsyncTask<Void, Void, List<Word>>{

        WordViewModel.iGetWords listener;
        WordDao wordDao;

        public GetAllWordsWithCallBackTask(WordDao wordDao, WordViewModel.iGetWords listener) {
            this.listener = listener;
            this.wordDao = wordDao;
        }

        @Override
        protected List<Word> doInBackground(Void... voids) {
            return wordDao.getAllWords();
        }

        @Override
        protected void onPostExecute(List<Word> words) {
            listener.gotWords(words);
        }
    }

get() has been removed and I simply execute the task, passing in listener to handle the call back:

  public void getAllWordsWithCallBack(WordViewModel.iGetWords listener) {
        try {
            new GetAllWordsWithCallBackTask(wordDao, listener).execute();
        } catch (Exception e) {
            Crashlytics.log("Getting all words exception: "+e.getMessage());
            e.printStackTrace();
        }
    }

This works well and the words are returned to my activity successfully, but the UI is still unresponsive while the operation is being executed.

Kes Walker
  • 1,154
  • 2
  • 10
  • 24
  • 1
    Have you considered RxJava? Also, if you don't mind working with Kotlin and are familiar with Coroutines, i'll advice you make use of that for multi-threading. Google just deprecated the AsyncTask API in favour of Coroutines. – Mayokun Nov 13 '19 at 19:55
  • @Mayokun oh yeah, didn't realize they did that :/ I'm planning on transferring to Kotlin in the future so I'll take a look at RxJava, thanks! – Kes Walker Nov 13 '19 at 20:04
  • Spawning the thread from ui thread as you are means the thread priority will inherit from ui thread meaning they are same priority - try `Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);` in the run method of spawned thread as first statement. –  Nov 13 '19 at 20:20
  • @Andy can only find `thread.setPriority(Thread.MIN_PRIORITY);` which didn't fix the issue. – Kes Walker Nov 13 '19 at 20:28
  • Technique: Start the 10 second operation, tap the screen, then break into the debugger using the "pause" button. Examine the UI thread stack trace to determine if/where it is blocking. P.S. I'm really surprised the progress bar stills spins. – greeble31 Nov 13 '19 at 21:34
  • if the problem still persists try on a different device with higher cores to be sure it isn't peculiar to the latter – linker Nov 13 '19 at 21:56
  • I definitely agree with @mayokun there are much better alternatives to sync task such as Coroutines :) – Doug Ray Nov 14 '19 at 21:20
  • @DougRay Can I make use of Kotlin Coroutines within my Java App? – Kes Walker Nov 14 '19 at 21:34
  • 1
    Why yes you can ! Thaaats what makes them sooooo great, AndroidTeam (TM). lol jk but no really Kotlin is fully inter-operable with java so you could definitely use just Coroutines if you want and have the rest be Java. – Doug Ray Nov 14 '19 at 22:10
  • @DougRay oh that's pretty cool, I might have a look at implementing coroutines then, thanks! – Kes Walker Nov 15 '19 at 08:32

2 Answers2

1

Edit 1 You call .get() on a AsyncTask. The calling thread waits for the AsyncTask to complete. You could implement interface callbacks to fix this problem.

Here is a solution for you're problem

Edit 2:
I took a closer look at your code, and again, there is no error in the code you posted here.

Using AsyncTask with callbacks is a possible solution. Your code runs in the background thread and the result is passed to the main thread without blocking it.

I think that your error lies in transferring the data from the callback to ViewModel or MainActivity.
The best solution to get around this is using LiveData.

I tried to rebuild your code as closely as possible. Maybe it will help you to find the mistake.

WordDb

@Database(entities = {Word.class}, version = 3)
@TypeConverters(DateConverter.class)
public abstract class WordDb extends RoomDatabase {

    private static WordDb INSTANCE;

    public abstract WordDao wordDao();

    static synchronized WordDb getInstance(Context contextPassed){
        if(INSTANCE == null){
            INSTANCE = Room.databaseBuilder(contextPassed.getApplicationContext(),WordDb.class,"word_db")
                    .fallbackToDestructiveMigration()
                    .build();
        }
        return INSTANCE;
    }
}

WordRepo

class WordRepo {
    private WordDao wordDao;

    WordRepo(Context applicationContext) {
        WordDb wordDb = WordDb.getInstance(applicationContext);
        wordDao = wordDb.wordDao();
    }

    void getAllWords(WordRepo.iGetWords listener) {
        try {
            Log.i("WordRepo", String.format("getAllWords() called from %s", Thread.currentThread().getName()));
            new GetAllWordsWithCallBackTask(wordDao, listener).execute();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class GetAllWordsWithCallBackTask extends AsyncTask<Void, Void, List<Word>> {

        WordRepo.iGetWords listener;
        WordDao wordDao;

        GetAllWordsWithCallBackTask(WordDao wordDao, WordRepo.iGetWords listener) {
            this.listener = listener;
            this.wordDao = wordDao;
        }

        @Override
        protected List<Word> doInBackground(Void... voids) {

            Log.i("WordRepo", String.format("GetAllWordsWithCallBackTask.doInBackground() called from %s", Thread.currentThread().getName()));
            return wordDao.getAll();
        }

        @Override
        protected void onPostExecute(List<Word> words) {
            Log.i("WordRepo", String.format("GetAllWordsWithCallBackTask.onPostExecute() called from %s", Thread.currentThread().getName()));

            listener.gotWords(words);
        }
    }

    public interface iGetWords {
        void gotWords(List<Word> words);
    }
}

MainViewModel

public class MainViewModel extends AndroidViewModel {

    MutableLiveData<List<Word>> wordList = new MutableLiveData<>();
    private static final String TAG = "MainViewModel";

    public MainViewModel(@NonNull Application application) {
        super(application);
    }

    void getAllWords() {

        Log.i(TAG, String.format("getAllWords() called from %s", Thread.currentThread().getName()));
        WordRepo repo = new WordRepo(getApplication());

        repo.getAllWords(new WordRepo.iGetWords() {
            @Override
            public void gotWords(List<Word> words) {
                wordList.setValue(words);
            }
        });
    }
}

getViewModelAndWords() in MainActivity

    private void getViewModelAndWords() {

        Log.i(TAG, String.format("getViewModelAndWords() called from %s", Thread.currentThread().getName()));

        viewModel = ViewModelProviders.of(this).get(MainViewModel.class);

        viewModel.wordList.observe(this, new Observer<List<Word>>() {
            @Override
            public void onChanged(List<Word> words) {
                //Do something with youre result
                Log.i(TAG, String.format("viewModel.wordList livedata returned %d results", words != null ? words.size() : -1));
            }
        });

        viewModel.getAllWords();


        Log.i(TAG, "viewModel.getAllWords() done");


    }

If you find out what is going wrong with youre code, please leave a comment

As @mayokun already mentioned i would recommend to use RxJava or migrating your project to Kotlin + Coroutines to keep your code nice an clean.

Here you can find more:

Medium - Coroutines on Android (part I): Getting the background

CodeLabs - Using Kotlin Coroutines in your Android App

Medium - RxAndroid Basics: Part 1

Medium - RxJava VS. Coroutines In Two Use Cases

I have successfully tested this code with about 300,000 records. Running this operation has blocked the Async Task on my emulator for about 30 sec. The main thread was accessible during this process.

I hope this works for you this time as well

Robin
  • 561
  • 1
  • 4
  • 16
  • I think the issue lies deeper within my code, please see EDIT to see what I mean. Replicating your code also worked for me and I was able to interact with the UI during the operation. So I think something within the database is causing this issue (even though I use `AsyncTask` for the db). – Kes Walker Nov 14 '19 at 18:35
  • @KesWalker Okay thanks for the feedback, I think i found the problem. Unfortunately, I can not try that for myself right now , but I have found a post that shows you a possible solution. Take a look at my EDIT above. – Robin Nov 14 '19 at 19:23
  • Unfortunately, that didn't work :/ please see new EDIT – Kes Walker Nov 14 '19 at 20:55
  • @KesWalker okay that is very strange. I will take a closer look at it tomorrow. But could you briefly explain what happens when you remove `.allowMainThreadQueries` from your Room Database builder? Does the App Crash with a `IllegalStateException`? – Robin Nov 14 '19 at 21:42
  • Just tried, doesn't seem to make a difference. The `get()` query still works, but im guessing that's because its ran from a worker thread. Also didn't crash when testing it with callback. – Kes Walker Nov 14 '19 at 21:52
  • Look at my new answer. Hope I could help you fix the problem – Robin Nov 15 '19 at 15:52
  • Thankyou for all your help and suggestions, I did already have an `allWords` live data method but didn't think of using it, but for this case it's worked great. Thanks again Rob :) – Kes Walker Nov 15 '19 at 17:49
1
  return new GetAllWordAsyncTask(wordDao).execute().get();

By calling get(), you are forcing the current invoking thread to synchronously wait for the result to come back, which makes your background query block the main thread while it executes.

The solution is to use a callback and onPostExecute rather than blocking the main thread to obtain your query results.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428