Search code examples
javaandroidandroid-roomandroid-listadapter

Why Inserting a new word in WordRoomDatabase codelab refresh all the list?


I don't get why the entire list is refresh (and jump to the top) instead of only the new added word. It's negating the purpose of SubmitList().

I tried to change the repository to manage a local LiveData<List< Word>> getAllWords without room and it worked well.

Is there something in room that cause the LiveData<List< Word>> getAllWords to completely refresh ? If so how to avoid it, It's ugly.

WORD REPOSITORY

   class WordRepository {
    
        private WordDao mWordDao;
        private LiveData<List<Word>> mAllWords;
    
        WordRepository(Application application) {
            WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
            mWordDao = db.wordDao();
            mAllWords = mWordDao.getAlphabetizedWords();
        }
    
        LiveData<List<Word>> getAllWords() {
            return mAllWords;
        }
    
        void insert(Word word) {
            WordRoomDatabase.databaseWriteExecutor.execute(() -> {
                mWordDao.insert(word);
            });
        }
    }

WORD VIEWMODEL

public class WordViewModel extends AndroidViewModel {

    private WordRepository mRepository;
    private final LiveData<List<Word>> mAllWords;

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

    LiveData<List<Word>> getAllWords() {
        return mAllWords;
    }

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

WORD DAO

public interface WordDao {

    @Query("SELECT * FROM word_table ORDER BY word ASC")
    LiveData<List<Word>> getAlphabetizedWords();

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    void insert(Word word);

    @Query("DELETE FROM word_table")
    void deleteAll();
}

WORD ADAPTER

public class WordListAdapter extends ListAdapter<Word, WordViewHolder> {

    public WordListAdapter(@NonNull DiffUtil.ItemCallback<Word> diffCallback) {
        super(diffCallback);
    }

    @Override
    public WordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return WordViewHolder.create(parent);
    }

    @Override
    public void onBindViewHolder(WordViewHolder holder, int position) {
        Word current = getItem(position);
        holder.bind(current.getWord());
    }

    static class WordDiff extends DiffUtil.ItemCallback<Word> {

        @Override
        public boolean areItemsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem == newItem;
        }

        @Override
        public boolean areContentsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem.getWord().equals(newItem.getWord());
        }
    }
}

MAIN ACTIVITY

public class MainActivity extends AppCompatActivity {

    public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 1;

    private WordViewModel mWordViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = findViewById(R.id.recyclerview);
        final WordListAdapter adapter = new WordListAdapter(new WordListAdapter.WordDiff());
        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        mWordViewModel = new ViewModelProvider(this).get(WordViewModel.class);

        mWordViewModel.getAllWords().observe(this, new Observer<List<Word>>() {
            @Override
            public void onChanged(List<Word> list) {
                adapter.submitList(list);
            }
        });

        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NewWordActivity.class);
                MainActivity.this.startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE);
            }
        });
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == NEW_WORD_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
            Word word = new Word(data.getStringExtra(NewWordActivity.EXTRA_REPLY));
            mWordViewModel.insert(word);
        } else {
            Toast.makeText(
                    getApplicationContext(),
                    R.string.empty_not_saved,
                    Toast.LENGTH_LONG).show();
        }
    }
}

Solution

  • First of all, check generated by Room library code

    @Override
    public LiveData<List<Word>> getAlphabetizedWords() {
        // ...
        return new ComputableLiveData<List<Word>>() {
            private Observer _observer;
    
            @Override
            protected List<Word> compute() {
                if (_observer == null) {
                    _observer = new Observer("word_table") {
                        @Override
                        public void onInvalidated(@NonNull Set<String> tables) {
                            invalidate();
                        }
                    };
                    __db.getInvalidationTracker().addWeakObserver(_observer);
                }
                final Cursor cursor = __db.query(_statement);
                final List<Word> _result = new ArrayList<Word>(cursor.getCount());
                while (cursor.moveToNext()) {
                    final Word item = createNewWordInstanceFromCursorData(cursor);
                    _result.add(item);
                }
                // ...
            }
        }.getLiveData();
    }
    

    After the first execution of the getAlphabetizedWords() method, the new table invalidation observer will be added. This Observer will be triggered after each transaction in a database table, such as inserting a new word or deleting one. So, after this, invalidate() method of ComputableLiveData will be called, which leads to the recomputation of the entire list of the words. LiveData value will be set to an entirely new instance of List with new instances of Word items.

    In WordDiff class areItemsTheSame() method is implemented to check object references. But in this case, we will get new Word instances and this implementation will return false. This leads to a full refresh of the list. In this case, you might change the implementation to the following

    static class WordDiff extends DiffUtil.ItemCallback<Word> {
    
        @Override
        public boolean areItemsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem.getWord().equals(newItem.getWord());
        }
    
        // ...
    }
    

    In more complex cases, you might check the unique data per elements, such as item id. See more: https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil.ItemCallback#areItemsTheSame(T,T)