0

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;
}      
14
  • There is not ` mViewModel.insertCard(card1); ` method in ViewModel. Share that part of code. Commented Nov 12, 2019 at 19:49
  • @GensaGames Will do, by 6pm ET USA. Commented Nov 12, 2019 at 20:44
  • @GensaGames ViewModel's insertCard() has been upated. And the same for the Repository and the Dao. Commented Nov 13, 2019 at 1:08
  • Can you move the Toast inside mViewModel.getMax().observe()? Your Toast might the firing before the data was saved and newMax value is update. @AJW Commented Nov 13, 2019 at 3:24
  • @Prokash Sarkar I understand your point but I am not sure how to move the Toast inside. Can you provide an example? Commented Nov 13, 2019 at 3:36

3 Answers 3

1
+100

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);
}
Sign up to request clarification or add additional context in comments.

6 Comments

Ah, ok. So how do I limit the Toast to only get triggered by the insert method (when a CardView is added to the database) so that it does not fire when other CRUD operations occur like a delete or update method?
I captured the existing MAX from the database and then compared it to the MAX after the insertCard() to determine that a new card was added. Answer accepted, upvoted and bounty awarded. Cheers to you.
Follow-up question: if I place the getMax() observer in the onCreate(), how can I determine that the update has been triggered by the database? So I can make sure I obtain the latest version not the data version.
I don't think getMax should be used to determine the correctness of insertCard. You should trust Room to always return the correct state from the database. You should use the result of the data base insertion call to verify if the insertion was successful.
Ok, so how do I make sure that the code to verify the insertion was successful runs just after the insertion? A button-click on the "Save" button runs the insertCard code. Just after the insert, I am trying to set up PendingIntents on the card to later fire alarms for Notifications. I am trying to get data from the newly inserted card (e.g., card.getId()) to set up the PendingIntents but I'm not sure how to get that data within the same code block of "onClickSave". I'd appreciate any insights or ideas on how to correct.
|
1

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

2 Comments

Ah, ok will give your fix a try. I should have the same Dao instance for all CRUD operations, correct? So deleteCard() and updateCard() should also be using "cardDao..." not their own unique Dao instance?
The Toast fired but it fired twice again, even after making one Dao instance for insert() and getMax(). See the revised code above based on your answer. First the Toast fired and said the previous card # was saved. Then it fired again and said the last card # was saved. So the second Toast correctly fired and showed the correct card # saved for the last card that was just inserted into the database. But the first Toast should not be firing because that CardView was previously saved. Any ideas on how to fix this?
0

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();
}

2 Comments

I would like the Toast to be fired after the user click inserts the new CardView data into the database. So wouldn't I have to move the observer code of mViewModel.getMax()... out of onCreate() and put it right after mViewModel.insertCard() in the onClickSave()? If not, how is the Toast triggered if it sits in onCreate()?
In addition, the app crashes when the List is empty and I try to add the first card using AddorUpdateCardActivity. The Logcat says the integer is null object reference.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.