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.
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;
}
}