8

Following some articles, I am trying to create an android app with the following:

  • A recyclerview which fetches data from room database using live data.
  • Data structure is a list of custom objects with an attribute for ordering the data.

Features in recyclerview:

  • Drag and drop for reordering data
  • Swipe to delete
  • UNDO action for swipe to delete

Referred articles:

  1. Google codelabs for room database and live data
  2. AndroidHive article for recyclerview swipe to delete and undo to restore deleted item
  3. Medium post by Paul Burke for drag/drop in recycler view
  4. Medium post by Paul Burke for customizing dragged item in recycler view
  5. SO Post to detect drop event in recycler view

My problem:
Data reordering is not updated in the room library.

Note:
I am using an attribute for data ordering

Please drop a comment if code of a particular file is required.I am not sure which code to post.

MainFragment.java (Code to reorder data, which is not working)

// To handle recycler view item dragging
@Override
public void onItemMove(int fromPosition, int toPosition) {

    // Log
    Log.e(TAG, "Item moved from " + fromPosition + " to " + toPosition);

    // Move the item within the recycler view
    mainRecyclerViewAdapter.moveItem(fromPosition, toPosition);
}

// To handle recycler view item drop
@Override
public void onItemDragged(int fromPosition, int toPosition) {

    // Log
    Log.e(TAG, "Item dragged from " + fromPosition + " to " + toPosition);

    mainActivityViewModel.moveWord(fromPosition, toPosition);
}

MainActivityViewModel.java

public void moveWord(int fromPosition, int toPosition) {

    // Move word
    wordRepository.move(fromPosition, toPosition);
}

WordRepository.java

public void move(int from, int to) {
    new moveAsyncTask(wordDao).execute(from, to);
}

// Async update task
private static class moveAsyncTask extends AsyncTask<Integer, Void, Void> {


    // Dao
    private WordDao asyncTaskDao;


    // Constructor
    moveAsyncTask(WordDao wordDao) {

        // Get dao
        asyncTaskDao = wordDao;
    }


    @Override
    protected Void doInBackground(final Integer... params) {

        int from = params[0];
        int to = params[1];

        if (from > to) {

            // Move upwards

            asyncTaskDao.getAllWordsBetween(to, from - 1).forEach(wordToUpdate -> {

                // Update word number
                wordToUpdate.decreaseSNo();

                // Update word in database
                update(wordToUpdate);
            });

            asyncTaskDao.getWordWithNo(from).forEach(wordToUpdate -> {

                // Update word number
                wordToUpdate.setSno(to);

                // Update word in database
                update(wordToUpdate);
            });

        } else {

            // Move downwards

            asyncTaskDao.getAllWordsBetween(from + 1, to).forEach(wordToUpdate -> {

                // Update word number
                wordToUpdate.increaseSNo();

                // Update word in database
                update(wordToUpdate);
            });

            asyncTaskDao.getWordWithNo(from).forEach(wordToUpdate -> {

                // Update word number
                wordToUpdate.setSno(to);

                // Update word in database
                update(wordToUpdate);
            });

        }

        return null;
    }
}

WordDao.java

@Query("SELECT * FROM words WHERE sno >= :low AND sno <= :high")
List<Word> getAllWordsBetween(int low, int high);

@Query("SELECT * FROM words WHERE sno == :sNo")
List<Word> getWordWithNo(int sNo);
Abhimanyu
  • 11,351
  • 7
  • 51
  • 121
  • Can you post your full code sample? The Github link is not working. – blueware Aug 28 '20 at 10:48
  • @blueware, The question is posted a year ago, and the repo has been made private after that as there were lots of changes done. The accepted answer solved my issue. Thanks. – Abhimanyu Aug 28 '20 at 11:31

2 Answers2

9

Here is an easy approach for ordering items, used it with Room Database.

Entity Object (Model):

A column for order, type Integer:

@ColumnInfo(name = "word_order")
private Integer mOrder;

In Dao:

A query, that retrieve the largest order in column word_order:

@Query("SELECT MAX(word_order) FROM word_table")
int getLargestOrder();

And a query that update an entire word list:

@Update(onConflict = OnConflictStrategy.REPLACE)
void update(List<WordEntity> wordEntities);

Used MAX SQL command to get the largest number.

In Activity (when adding new word):

Query that method getLargestOrder() and added +1 to it, then create a new word.

In Activity (onCreate()):

Create ItemTouchHelper to move word items, use only UP & DOWN movement:

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP | ItemTouchHelper.DOWN) {

});

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.

Override getMovementFlags inside it to specified the wanted directions:

        @Override
        public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
                    0);
        }

Override onMove inside it to swap item positions:

        @Override
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            int fromPosition = viewHolder.getAdapterPosition();
            int toPosition = target.getAdapterPosition();

            if (fromPosition < toPosition) {
                for (int i = fromPosition; i < toPosition; i++) {
                    Collections.swap(mWordEntities, i, i + 1);

                    int order1 = mWordEntities.get(i).getOrder();
                    int order2 = mWordEntities.get(i + 1).getOrder();
                    mWordEntities.get(i).setOrder(order2);
                    mWordEntities.get(i + 1).setOrder(order1);
                }
            } else {
                for (int i = fromPosition; i > toPosition; i--) {
                    Collections.swap(mWordEntities, i, i - 1);

                    int order1 = mWordEntities.get(i).getOrder();
                    int order2 = mWordEntities.get(i - 1).getOrder();
                    mWordEntities.get(i).setOrder(order2);
                    mWordEntities.get(i - 1).setOrder(order1);
                }
            }
            mAdapter.notifyItemMoved(fromPosition, toPosition);
            return true;
        }

This method where items position get updated when moved.

  • First, get the positions for items from the adapter.

  • Then, if decrement, swap the items of Entities, by passing the current item position and the new one (for user eyes).

  • After that, get the order the current item and the next item, and swab them and set them with setter methods (for saving them in Room later).

  • On the other hand, do same for increment.

  • Finishing, by notify the adapter items has moved (for user eyes).

Override clearView to update Word Entities List:

        @Override
        public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
            mWordViewModel.update(mWordEntities);
        }

This method called when user drop the item. It will be suitable to update and save the entities into the Room Database.

Override onSwiped:

        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        }

This must be override by the system.

Attache ItemTouchHelper to the RecyclerView:

itemTouchHelper.attachToRecyclerView(mRecyclerView);

You can create many ItemTouchHelper and attach them to the RecyclerView as you need.

Beware that you need to order items in RecyclerView from Biggest number to Lowest number. Hope this help you :)

MohammadL
  • 2,398
  • 1
  • 19
  • 36
  • Where does mwordentities come from? – Pedro Pinheiro Jun 10 '20 at 11:38
  • @PedroPinheiro that is a global variable which holds a list of all the word entities, so the updated list can later be feed into the `mWordViewModel` which allows the database to update accordingly – marticztn Jul 15 '21 at 00:34
4

Hi:) I would do it like this:

Ad.1. Moving items

  1. I would add a "position" column to your Word object, and I would do it equal to your words.size(). That would make it "autoincrement" but not necessarily unique (which is important).

    So when you add new Word to your database, you do it like this: wordsDao.insert(new Word(position, whateverYouNeedToAdd));

  2. In your WordsDao:

    @Query("SELECT * FROM word ORDER BY position ASC")
    LiveData<List<Word>> sortByPosition();
    
  3. In ViewModel:

    public WordsViewModel(@NonNull Application application) {
            super(application);
            wordsDao = getDatabase(application).wordsDao();
            wordsLiveData = wordsDao.sortByPosition();
    }
    
    public LiveData<List<Word>> getWordsList() {
         return wordsLiveData;
    }
    
  4. In activity where you display your list:

    public void initData(){
       wordsViewModel = ViewModelProviders.of(this).get(WordsViewModel.class);
       wordsViewModel.getWordsList().observe(this, new Observer<List<Word>>() {
          @Override
          public void onChanged(@Nullable List<Word> words) {
              adapter.setWordsList(words);
              if (words == null) {
                  position = 0;
              } else {
                  position = words.size();
              }
          }
       });
    }
    
  5. In your adapter:

    void setWordsList(List<Word> wordsList) {
        this.wordsList = wordsList;
        notifyDataSetChanged();
    }
    
  6. To move the items:

     public void onItemMove(int fromPosition, int toPosition) {
        if (fromPosition < toPosition) {
            for (int i = fromPosition; i < toPosition; i++) {
                Collections.swap(wordsList, i, i + 1);
            }
    
        } else {
            for (int i = fromPosition; i > toPosition; i--) {
                Collections.swap(wordsList, i, i - 1);
            }
         }
        notifyItemMoved(fromPosition, toPosition);
    }
    
  7. I would create a method in adapter in which you update positions in database:

     void setIndexInDatabase() {
        WordsDao wordsDao = getDatabase(context).wordsDao();
        for (Word word : wordsList) {
            word.setPosition(wordsList.indexOf(word));
            wordsDao.update(word);
        }
    
  8. I would call that method in the activity where you display your list in it's onDestroy:

     @Override
     public void onDestroy() {
        super.onDestroy();
        adapter.setIndexInDatabase();
     }
    

So now the user can move the items as long as he wants, and when he finishes and goes to another activity the new order is saved in your database.

Ad.2. Delete Items

  1. First I would create an Arraylist in your adapter to store a list of deleted items in it

    private ArrayList<String> deletedWords = new ArrayList<>();
    
  2. Then I would create delete and undo methods:

    public void deleteItem(int position) {
        word = wordsList.get(position);
        wordPosition = position;
        deletedWord = word.getWord();
        deletedWords.add(deletedWord);
        wordsList.remove(position);
        notifyItemRemoved(position);
        showUndoSnackbar();
    }
    

    I'm adding a word which you want to delete to this array we created.

    private void showUndoSnackbar() {
    
        Snackbar snackbar = Snackbar.make( (((Activity) context).findViewById(android.R.id.content)), R.string.snack_bar_text,
                Snackbar.LENGTH_LONG);
        snackbar.setAction(R.string.snack_bar_undo, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                undoDelete();
            }
        });
        snackbar.show();
    }
    
    private void undoDelete() {
        wordsList.add(wordPosition,
                word);
        deletedWords.remove(deletedWord);
        notifyItemInserted(wordPosition);
    }
    

    In undoDelete I'm removing this deleted word from our ArrayList

  3. I would create a method to delete all this words stored in our ArrayList from database:

    void deleteFromDatabase() {
        WordsDao wordsDao = getDatabase(context).wordsDao();
        if (deletedWords != null) {
            for (int i = 0; i < deletedWords.size(); i++) {
                wordsDao.deleteByWord(deletedWords.get(i));
            }
        }
    }
    
  4. I would call it in the onDestroy like before:

    @Override
    public void onDestroy() {
        super.onDestroy();
        adapter.setIndexInDatabase();
        adapter.deleteFromDatabase();
    }
    

I hope I didn't forget about anything :) I did something like that in app I'm writing now, so you can check it out also on github.

I use it in Shift package: ShiftFragment and ShiftAdapter.

GuilhermeMagro
  • 321
  • 1
  • 10