问题
I have a RecyclerView list of CardView items that is working properly. Upon creation of a new CardView that is inserted into the database, I would like to fire a Toast that informs the user that the CardView was successfully added and show the CardView number. The CardView number is the Id of the CardView item inserted into the database. The data is saved to the database when the user clicks on a Save button that fires onClickSave().
I set up an @Query in the Dao to get the MAX(cardId):
Dao
...
@Query("SELECT MAX(cardId) FROM cards")
LiveData<Integer> getMax();
@Insert
void insertCard(Card card);
Problem is that two Toasts are firing. The first Toast is returning the previously created CardView number and then the second Toast is firing and it shows the latest CardView number that was just added. For example, the Toast will show CardView number 33 and then a second Toast fires that shows the expected CardView number 34 that was just created (I confirm that CardViews 33 and 34 are both in the database and the two highest items, using DB Browser for SQLite software).
AddorUpdateCardActivity
...
private int newMax = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewModel = new ViewModelProvider(this).get(cardViewModel.class);
}
public void onClickSave(View v) {
// set card data
// then insert data in database
mViewModel.insertCard(card1);
mViewModel.getMax().observe(this, value -> { newMax = value; Toast.makeText(AddorUpdateCardActivity.this, "card #" + newMax + " was saved to the list", Toast.LENGTH_LONG).show();});
}
ViewModel
...
public cardViewModel(Application application) {
super(application);
repository = new cardRepository(application);
getMax = repository.getMax();
}
public LiveData<Integer> getMax() {
return getMax;
}
public void insertCard(Card card) {
repository.insertCard(card);
}
cardRepository
private CardDao cardDao;
private LiveData<Integer> getMax;
public cardRepository(Application application) {
RoomDatabase db = RoomDatabase.getDatabase(application);
cardDao = db.cardDao();
}
public LiveData<Integer> getMax() {
return cardDao.getMax;
}
public void insertCard(Quickcard newcard) {
AsyncTask.execute(() -> cardDao.insertCard(newcard));
}
What am I missing here? If the card is inserted properly into the database then why wouldn't the ViewModel observer just return this new CardView number rather than two Toasts?
For reference, I show the previous code I used prior to Room and ViewModel that used a cursor to get the latest and highest inserted Id:
public class SQLiteDB extends SQLiteOpenHelper {
...
public int getLastInsertId() {
int index = 0;
SQLiteDatabase sdb = getReadableDatabase();
Cursor cursor = sdb.query(
"sqlite_sequence",
new String[]{"seq"},
"name = ?",
new String[]{TABLE_NAME},
null,
null,
null,
null
);
sdb.beginTransaction();
try {
if (cursor !=null) {
if (cursor.moveToLast()) {
index = cursor.getInt(cursor.getColumnIndex("seq"));
}
}
...
}
return index;
}
回答1:
The view model operations you call within onClickSave
are asynchronous:
public void onClickSave(View v) {
mViewModel.insertCard(card1);
mViewModel.getMax().observe(this, value -> { newMax = value; makeText(AddorUpdateCardActivity.this, "TEXT", .LENGTH_LONG).show();});
}
The implementation of LiveData
records the data version as well as the last version seen by the observer.
Therefore insertCard
starts to operate on a worker thread while you start observing getMax
from the main thread with a newly created observer. Thus you'll receive the current value as well as the new value after the database was updated.
Instead you could observe it only once in onCreate()
and wait for the updates triggered by the database:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewModel = new ViewModelProvider(this).get(cardViewModel.class);
mViewModel.getMax().observe(this, value -> { newMax = value; makeText(AddorUpdateCardActivity.this, "TEXT", .LENGTH_LONG).show();});
}
public void onClickSave(View v) {
mViewModel.insertCard(card1);
}
回答2:
The Room Insert
operation inside AsyncTask
takes a while before the maxCount
variable is updated. Since you are showing the Toast
inside a button click, the message is displayed right away without receiving the updated value from LiveData
.
Move the Toast
message inside the obverve()
method so that it gets triggered only after a LiveData
change.
mViewModel.getMax().observe(this, value -> {
newMax = value;
Toast.makeText(AddorUpdateCardActivity.this, "card #" + newMax + " was saved to the list", Toast.LENGTH_LONG).show();
});
At this point, the code should be working but you'll get multiple LiveData
events for a single Insert
. This is happening because you have used 2 separate instances of Dao for Insert
and Query
operation.
public cardRepository(Application application) {
RoomDatabase db = RoomDatabase.getDatabase(application);
cardDao = db.cardDao(); // <---------- Instance #1
getMax = cardDao.getMax();
}
public LiveData<Integer> getMax() {
return getMax;
}
public void insertCard(Card newcard) {
new InsertAsyncTask(quickcardDao).execute(newcard);
}
private static class InsertAsyncTask extends AsyncTask<Card, Void, Integer> {
private CardDao asyncTaskDao;
InsertAsyncTask(CardDao dao) {
asyncTaskDao = dao; // <---------- Instance #2
}
@Override
protected Integer doInBackground(final Card... params) {
asyncTaskDao.insertCard(params[0]);
return null;
}
}
To resolve it use the same Dao instance everywhere:
public cardRepository(Application application) {
RoomDatabase db = RoomDatabase.getDatabase(application);
cardDao = db.cardDao();
}
public LiveData<Integer> getMax() {
return cardDao.getMax();
}
public void insertCard(Card newcard) {
AsyncTask.execute(() -> cardDao.insertCard(newcard));
}
回答3:
Because of using AsyncTask to insert card to database, that function take some time to complete and you show your toast, instantly! Change your activity to this:
AddorUpdateCardActivity
...
private int newMax = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewModel = new ViewModelProvider(this).get(cardViewModel.class);
mViewModel.getMax().observe(this, integer2 -> {
newMax = integer2;
Toast.makeText(AddorUpdateCardActivity.this, "card #" + newMax + " was saved to the list", Toast.LENGTH_LONG).show();
hideProgressBar();
});
}
public void onClickSave(View v) {
//set card data
// then insert data in database
mViewModel.insertCard(card1);
showProgressBar();
}
来源:https://stackoverflow.com/questions/58790802/room-database-getting-select-max-twice-after-creating-new-recyclerview-item