javaandroidrx-javarx-android

Android - RxJava - Race condition


I am new to the concept of multi-threading and also new to RxJava. I think I ran into a race condition in my Android project. To be able to demonstrate it better, I downloaded the google codelabs example RoomWithAView and implemented the same race condition as in my own project.

WordDao

@Dao
public interface WordDao {
    @Insert()
    void insert(Word word);

    @Query("SELECT EXISTS(SELECT word FROM WORD_TABLE WHERE word = :word)")
    Single<Integer> wordExists(String word);
}

WordRepository

class WordRepository {

    private WordDao mWordDao;
    private boolean mWordRedundant;
    private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();

    WordRepository(Application application) {
        WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
        mWordDao = db.wordDao();
    }

    void insert(Word word) {
        WordRoomDatabase.databaseWriteExecutor.execute(() -> {
            mWordDao.insert(word);
        });
    }

    public boolean isWordRedundant(String word) {
        mCompositeDisposable.add(
        mWordDao.wordExists(word)
                .subscribeOn(Schedulers.computation())
                .subscribe(integer -> mWordRedundant = integer == 1));
        return mWordRedundant;
    }

WordViewModel

public class WordViewModel extends AndroidViewModel {

    private WordRepository mRepository;

    public WordViewModel(Application application) {
        super(application);
        mRepository = new WordRepository(application);
    }

    public boolean isWordRedundant(String word) {
        return mRepository.isWordRedundant(word);
    }

    public void insert(Word word) {
        mRepository.insert(word);
    }
}

NewWordActivity

public class NewWordActivity extends AppCompatActivity {

    public static final String EXTRA_REPLY = "com.example.android.wordlistsql.REPLY";

    private EditText mEditWordView;
    private WordViewModel mWordViewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_new_word);
        mWordViewModel = new ViewModelProvider(this).get(WordViewModel.class);
        mEditWordView = findViewById(R.id.edit_word);

        final Button button = findViewById(R.id.button_save);
        button.setOnClickListener(view -> {
            Intent replyIntent = new Intent();

            if (TextUtils.isEmpty(mEditWordView.getText())) {
                System.out.println("Word empty");
                setResult(RESULT_CANCELED, replyIntent);
            }

            // possible race condition?
            else if (mWordViewModel.isWordRedundant(mEditWordView.getText().toString())) {
                System.out.println("Word redundant");
                setResult(RESULT_CANCELED, replyIntent);
            }

            else {
                String word = mEditWordView.getText().toString();
                System.out.println("Word acceptable");
                replyIntent.putExtra(EXTRA_REPLY, word);
                setResult(RESULT_OK, replyIntent);
            }
            finish();
        });
    }
}

The method isPlayerNameRedundant() always returns false in my case. I tested the SQL query in App Inspection and it returns 1, so the isPlayerNameRedundant() method should return true. I suspect that since I use Schedulers.computation() in the repository the query is being executed on a background thread and before this thread is finished with its task the main thread returns mWordRedundant. Is that correct?

If so, what would be the way to resolve this?

Thank you in advance.


Solution

  • You are misusing Rx-java.

    Correct way:

    WordRepository

    public Single<Boolean> isWordRedundant(String word) {
        return mWordDao.wordExists(word)
                .subscribeOn(Schedulers.io()) //computation() is for CPU intensive operation. Not for DB read/write. This is a tiny error.
                .map(i -> i == 1 ? true : false)
    }
    

    WordViewModel

    public Single<Boolean> isWordRedundant(String word) {
        return mRepository.isWordRedundant(word);
    }
    

    NewWordActivity

        button.setOnClickListener(view -> {
            Intent replyIntent = new Intent();
    
            if (TextUtils.isEmpty(mEditWordView.getText())) {
                System.out.println("Word empty");
                setResult(RESULT_CANCELED, replyIntent);
                finish();
            } else {
                //Your CompositDisposable in activity
                compositDisposable.add(
                    mWordViewModel.isWordRedundant(mEditWordView.getText().toString())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(isRedundant -> {
                            if (isRedundant) {
                                System.out.println("Word redundant");
                                setResult(RESULT_CANCELED, replyIntent);
                            } else {
                                String word = mEditWordView.getText().toString();
                                System.out.println("Word acceptable");
                                replyIntent.putExtra(EXTRA_REPLY, word);
                                setResult(RESULT_OK, replyIntent);
                            }
                            finish();
                        }, error -> {/* handle error here. Maybe a Toast or something. */})
                );
            }
        });
    

    So why your before code is not working? Let's see the code in WordRepository before (note the comments):

    
    class WordRepository {
    
        private WordDao mWordDao;
        private boolean mWordRedundant; //field variable. Not initiate. So is the default value: **false**.
        private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();
    
        public boolean isWordRedundant(String word) {
            //Main thread enter here.
            //Main thread step 1: Create an Observable i.e.(mWordDao.wordExists(word)). And subscribe on it.
            mCompositeDisposable.add(
                mWordDao.wordExists(word)
                    .subscribeOn(Schedulers.computation())
                    .subscribe(integer -> mWordRedundant = integer == 1)); //We process it and observe the result in computation() thread. Then change the filed variable "mWordRedundant" value.
            
            //Main thread step 2: return the field variable **immediately**! Not wait for the result from computation() thread. That is why you can not get the right result.
            return mWordRedundant;
        }
    }