Почему мой SelectionTracker продолжает удерживать элементы несмотря на то, что они уже удалены из Room?
Если коротко, я пытаюсь реализовать удаление нескольких элементов списка при помощи множественного выделения. Для хранения списка используется Room, для отображения — RecyclerView с кастомным ListAdapter и ViewHolder. Для реализации множественного выделения используется SelectionTracker с кастомными KeyProvider и ItemDetailsLookup. Выделенные элементы, помещаются в специальную коллекцию внутри ViewModel для дальнейшей обработки.
Отрисовка данных происходит корректно, запросы на удаление нескольких элементов успешно обрабатываются (по крайней мере в пределах малого числа элементов). Беспокоит меня в данный момент только одно: по какой-то причине, несмотря на то, что выделенные элементы успешно удаляются и из базы данных, и из RecyclerView соответственно, SelectionTracker не хочет очищать свою коллекцию — счётчик выделенных элементов не изменяется, метод hasSelection() возвращает true.
Я пробовал создать observer для коллекции во ViewModel, отвечающей за хранение выделенных элементов, но условие isEmpty не срабатывало, хотя я вызывал метод очистки той коллекции.
Journal.java
@Entity(tableName = "journal_table")
public class Journal {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "title")
private String title;
public Journal(String title) {
this.title = title;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Journal) {
Journal j2 = ((Journal) obj);
boolean c1 = (this.id == j2.getId());
boolean c2 = (this.title.contentEquals(j2.title));
return c1 && c2;
} else {
return obj.equals(this);
}
}
}
JournalRepository.java
class JournalRepository {
private JournalDao journalDao;
private LiveData<List<Journal>> allJournals;
JournalRepository(Application application) {
JournalDatabase database = JournalDatabase.getInstance(application);
journalDao = database.journalDao();
allJournals = journalDao.getAllJournals();
}
void insert(Journal journal) {
new InsertJournalAsyncTask(journalDao).execute(journal);
}
void delete(Journal journal) {
new DeleteJournalAsyncTask(journalDao).execute(journal);
}
LiveData<List<Journal>> getAllJournals() {
return allJournals;
}
private static class InsertJournalAsyncTask extends AsyncTask<Journal, Void, Void> {
private JournalDao journalDao;
private InsertJournalAsyncTask(JournalDao journalDao) {
this.journalDao = journalDao;
}
@Override
protected Void doInBackground(Journal... journals) {
journalDao.insert(journals[0]);
return null;
}
}
private static class DeleteJournalAsyncTask extends AsyncTask<Journal, Void, Void> {
private JournalDao journalDao;
private DeleteJournalAsyncTask(JournalDao journalDao) {
this.journalDao = journalDao;
}
@Override
protected Void doInBackground(Journal... journals) {
journalDao.delete(journals[0]);
return null;
}
}
}
JournalViewModel.java
public class JournalViewModel extends AndroidViewModel {
private JournalRepository repository;
private LiveData<List<Journal>> allJournals;
private LiveData<List<Journal>> selectedJournals = new MutableLiveData<>(new LinkedList<>());
public JournalViewModel(@NonNull Application application) {
super(application);
repository = new JournalRepository(application);
allJournals = repository.getAllJournals();
}
public void insert(Journal journal) {
repository.insert(journal);
}
public void delete(Journal journal) {
repository.delete(journal);
}
public void deleteSelectedJournals() {
for (Journal j :
Objects.requireNonNull(selectedJournals.getValue())) {
delete(j);
}
selectedJournals.getValue().clear();
}
public void addToSelection(Journal journal) {
Objects.requireNonNull(selectedJournals.getValue()).add(journal);
}
public void removeFromSelection(Journal journal) {
Objects.requireNonNull(selectedJournals.getValue()).remove(journal);
}
public void clearSelection() {
Objects.requireNonNull(selectedJournals.getValue()).clear();
}
public LiveData<List<Journal>> getAllJournals() {
return allJournals;
}
public List<Journal> getFilteredJournals(String filter) {
return repository.getFilteredJournals(filter);
}
public LiveData<List<Journal>> getSelectedJournals() {
return selectedJournals;
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
Resources resources;
JournalViewModel viewModel;
JournalAdapter adapter;
SelectionTracker<Long> tracker;
ActionMode actionMode;
RecyclerView recyclerView;
FloatingActionButton buttonCreate;
private ActionMode.Callback callback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_main_selection, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
mode.setTitle(resources.getString(R.string.selected_count_title, tracker.getSelection().size()));
buttonCreate.setVisibility(View.GONE);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.select_all:
tracker.setItemsSelected(adapter.getAllIds(), true);
return true;
case R.id.delete_selected:
viewModel.deleteSelectedJournals();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
tracker.clearSelection();
viewModel.clearSelection();
actionMode = null;
buttonCreate.setVisibility(View.VISIBLE);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
resources = getResources();
adapter = new JournalAdapter();
viewModel = new ViewModelProvider(this).get(JournalViewModel.class);
recyclerView = findViewById(R.id.recycler_view);
buttonCreate = findViewById(R.id.button_create_journal);
LinearLayoutManager llm = new LinearLayoutManager(this);
recyclerView.setLayoutManager(llm);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
buttonCreate.setOnClickListener(view -> openCreateDialog());
adapter.setOnSelectionChangedListener((journal, isChecked) -> {
if (isChecked) {
viewModel.addToSelection(journal);
} else {
viewModel.removeFromSelection(journal);
}
});
viewModel.getAllJournals().observe(this, adapter::submitList);
viewModel.getAllJournals().observe(this, journals -> {
if (journals.size() == 0) {
if (actionMode != null) {
actionMode.finish();
}
}
});
tracker = new SelectionTracker.Builder<>(
"selectedJournalsTrackerId",
recyclerView,
new JournalKeyProvider(adapter, recyclerView),
new JournalDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
)
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
tracker.addObserver(new SelectionObserver<Long>() {
@Override
public void onSelectionChanged() {
if (tracker.hasSelection()) {
if (actionMode == null) {
actionMode = startSupportActionMode(callback);
} else {
actionMode.setTitle(resources.getString(R.string.selected_count_title, tracker.getSelection().size()));
}
} else if (!tracker.hasSelection()) {
if (actionMode != null) {
actionMode.finish();
}
}
}
});
adapter.setTracker(tracker);
viewModel.getSelectedJournals().observe(this, list -> {
if (list.isEmpty()) {
tracker.clearSelection();
}
});
}
}
JournalAdapter.java
public class JournalAdapter extends ListAdapter<Journal, JournalAdapter.JournalViewHolder> {
private static final DiffUtil.ItemCallback<Journal> DIFF_CALLBACK = new DiffUtil.ItemCallback<Journal>() {
@Override
public boolean areItemsTheSame(@NonNull Journal oldItem, @NonNull Journal newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Journal oldItem, @NonNull Journal newItem) {
return oldItem.equals(newItem);
}
};
private OnSelectionChangedListener selectionChangedListener;
private SelectionTracker<Long> tracker;
public JournalAdapter() {
super(DIFF_CALLBACK);
setHasStableIds(true);
}
@NonNull
public JournalViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_journal, parent, false);
return new JournalViewHolder(itemView, itemClickListener, selectionChangedListener);
}
public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
selectionChangedListener = listener;
}
public void setTracker(SelectionTracker<Long> tracker) {
this.tracker = tracker;
}
@Override
public void onBindViewHolder(@NonNull JournalViewHolder holder, int position) {
Journal current = getItem(position);
holder.textViewTitle.setText(current.getTitle());
if (tracker != null) {
if (tracker.hasSelection()) {
holder.checkBox.setChecked(tracker.isSelected(getItemId(holder.getAdapterPosition())));
}
}
}
@Override
public long getItemId(int position) {
return getItem(position).getId();
}
public List<Long> getAllIds() {
List<Long> list = new LinkedList<>();
for (int i = 0; i < getItemCount(); i++) {
list.add(getItemId(i));
}
return list;
}
public interface OnSelectionChangedListener {
void onSelectionChanged(Journal journal, boolean isChecked);
}
class JournalViewHolder extends RecyclerView.ViewHolder {
private TextView textViewTitle;
private CheckBox checkBox;
JournalViewHolder(@NonNull View itemView, final OnSelectionChangedListener scListener) {
super(itemView);
textViewTitle = itemView.findViewById(R.id.title);
checkBox = itemView.findViewById(R.id.checkBox);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
int position = getAdapterPosition();
if (buttonView != null && position != RecyclerView.NO_POSITION) {
scListener.onSelectionChanged(getItem(position), isChecked);
}
});
tracker.addObserver(new SelectionTracker.SelectionObserver() {
@Override
public void onSelectionChanged() {
if (tracker.hasSelection()) {
checkBox.setVisibility(View.VISIBLE);
} else if (!tracker.hasSelection()) {
checkBox.setChecked(false);
checkBox.setVisibility(View.GONE);
}
}
});
}
}
}
JournalDetailsLookup.java
public final class JournalDetailsLookup extends ItemDetailsLookup<Long> {
private final RecyclerView mRecyclerView;
public JournalDetailsLookup(RecyclerView mRecyclerView) {
this.mRecyclerView = mRecyclerView;
}
@Nullable
@Override
public ItemDetails<Long> getItemDetails(@NonNull MotionEvent e) {
View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
if (holder instanceof JournalAdapter.JournalViewHolder) {
final JournalAdapter.JournalViewHolder journalHolder = (JournalAdapter.JournalViewHolder) holder;
return new ItemDetails<Long>() {
@Override
public int getPosition() {
return journalHolder.getAdapterPosition();
}
@NonNull
@Override
public Long getSelectionKey() {
return journalHolder.getItemId();
}
};
}
}
return null;
}
}
JournalKeyProvider.java
public class JournalKeyProvider extends ItemKeyProvider<Long> {
private JournalAdapter adapter;
private RecyclerView recyclerView;
public JournalKeyProvider(JournalAdapter adapter, RecyclerView recyclerView) {
super(SCOPE_MAPPED);
this.adapter = adapter;
this.recyclerView = recyclerView;
}
@Nullable
@Override
public Long getKey(int position) {
return adapter.getItemId(position);
}
@Override
public int getPosition(@NonNull Long key) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForItemId(key);
return viewHolder == null ? RecyclerView.NO_POSITION : viewHolder.getLayoutPosition();
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:scrollbars="vertical"
tools:listitem="@layout/card_journal" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
card_journal.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="6dp"
app:cardElevation="0dp"
app:contentPadding="16dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end|center_vertical"
android:visibility="gone"
android:clickable="false" />
</androidx.cardview.widget.CardView>
Пример поведения программы, от которого я хочу избавиться (YouTube)