I'm currently building an app which use a RealmRecyclerViewAdapter
for displaying the elements inside Realm
.
I was looking into implementing the Filterable
interface, which I managed to do (thanks to those answers: Link 1 Link 2) but now I have a side effect: when I'm filtering, the Adapter
shows all the elements, even if they doesn't match with the filter. Also, the excluded element does show incorrect information. When I close the SearchView
, everything is back to normal.
Here is the Activity
when I call the Adapter
:
public class MainActivity extends AppCompatActivity {
private Realm realm;
HabitCardAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
setUIMode();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set the title inside the top bar for this activity.
// I'm not doing it inside the Manifest because it changes the app's name
setTitle(R.string.MainActivityTitle);
// Bottom App Bar setup
BottomAppBar bottomAppBar = findViewById(R.id.bottomAppBar);
cutBottomAppEdge(bottomAppBar); // Diamond shape
// Add listener to Stats button inside the bottom app bar
MenuItem statsMenuItem = bottomAppBar.getMenu().findItem(R.id.statsMenuItem);
statsMenuItem.setOnMenuItemClickListener(item -> {
if(item.getItemId() == R.id.statsMenuItem){
Intent i = new Intent(getApplicationContext(), StatsActivity.class);
startActivity(i);
return true;
}
return false;
});
// FAB button setup
FloatingActionButton fab = findViewById(R.id.fabAddButton);
fab.setOnClickListener(view -> {
Intent intent = new Intent(getBaseContext(), CreateHabitActivity.class);
startActivity(intent);
});
RecyclerView rv = findViewById(R.id.habitCardRecyclerView);
TextView emptyMessage = findViewById(R.id.mainEmptyHabitListMessage);
realm = Realm.getDefaultInstance();
RealmResults<Habit> results = realm.where(Habit.class).sort("id").findAll();
results.addChangeListener(habits -> {
if (habits.size() > 0) {
rv.setVisibility(View.VISIBLE);
emptyMessage.setVisibility(View.GONE);
} else {
emptyMessage.setVisibility(View.VISIBLE);
rv.setVisibility(View.GONE);
}
});
//this is necessarily because it is not changed yet
if (results.size() > 0) {
rv.setVisibility(View.VISIBLE);
emptyMessage.setVisibility(View.GONE);
} else {
emptyMessage.setVisibility(View.VISIBLE);
rv.setVisibility(View.GONE);
}
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
rv.setLayoutManager(layoutManager);
adapter = new HabitCardAdapter(results, true, this, realm);
rv.setAdapter(adapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.top_app_bar_menu, menu);
SearchView searchView = (SearchView) menu.findItem(R.id.searchMenuItem).getActionView();
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
adapter.getFilter().filter(query);
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
if (adapter != null) {
adapter.getFilter().filter(newText);
return true;
}
return false;
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.searchMenuItem:
return true;
case R.id.settingMenuItem:
Intent intent = new Intent(getApplicationContext(), SettingsActivity.class);
startActivity(intent); //FIXME: animazione
return true;
case R.id.aboutMenuItem:
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(getString(R.string.about_us_title));
builder.setMessage(getString(R.string.about_us_message));
builder.setIcon(R.drawable.ic_sprout_fg_small);
builder.setPositiveButton("OK", (dialogInterface, i) -> {
dialogInterface.dismiss();
});
builder.show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Set the Night/Light UI. On the first run of the app, the user get the Light UI.
*/
private void setUIMode() {
SharedPreferences preferences = getSharedPreferences(SettingsActivity.SHARED_PREFS_FILE, MODE_PRIVATE);
int pref = preferences.getInt(SettingsActivity.SHARED_PREFS_DARK_MODE, AppCompatDelegate.MODE_NIGHT_NO);
AppCompatDelegate.setDefaultNightMode(pref);
}
private void cutBottomAppEdge(BottomAppBar bar) {
BottomAppBarTopEdgeTreatment topEdge = new SproutBottomAppBarCutCornersTopEdge(
bar.getFabCradleMargin(),
bar.getFabCradleRoundedCornerRadius(),
bar.getCradleVerticalOffset());
MaterialShapeDrawable babBackground = (MaterialShapeDrawable) bar.getBackground();
//It requires 1.1.0-alpha10
babBackground.setShapeAppearanceModel(
babBackground.getShapeAppearanceModel()
.toBuilder()
.setTopEdge(topEdge)
.build());
}
@Override
protected void onDestroy() {
super.onDestroy();
realm.removeAllChangeListeners();
realm.close();
}
}
Here is the HabitCardAdapter
which extends RealmRecyclerViewAdapter
:
public class HabitCardAdapter extends RealmRecyclerViewAdapter<Habit, HabitCardAdapter.ViewHolder> implements Filterable {
Context ct;
OrderedRealmCollection<Habit> list;
Realm mRealm;
public HabitCardAdapter(@Nullable OrderedRealmCollection<Habit> data, boolean autoUpdate, Context context, Realm realm) {
super(data, autoUpdate); //autoUpdate to true
ct = context;
list = data;
mRealm = realm;
}
@Override
public int getItemCount() {
return this.list.size();
}
@NonNull
@Override
public HabitCardAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
//TODO: inflatare diversi tipi di carte a seconda del habitType
View view = inflater.inflate(R.layout.fragment_habit_counter_card, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull HabitCardAdapter.ViewHolder holder, int position) {
final Habit habit = getItem(position);
if (habit != null) {
holder.setHabit(habit);
holder.editHabitButton.setOnClickListener(view -> {
Intent intent = new Intent(ct, EditHabitActivity.class);
intent.putExtra("HABIT_ID", habit.getId());
//TODO: Aggiungere l'animazione
ct.startActivity(intent);
});
holder.checkButton.setOnClickListener(view -> {
int habitId = habit.getId();
int newRepValue = habit.getRepetitions() + 1;
int maxReps = habit.getMaxRepetitions();
Log.d("Testing", newRepValue + " - " + maxReps);
if (newRepValue <= habit.getMaxRepetitions()) {
habit.getRealm().executeTransaction(realm -> {
Habit result = realm.where(Habit.class).equalTo("id", habitId).findFirst();
if (result != null) {
result.setRepetitions(newRepValue);
String newLabel = "Completato " + newRepValue + " volte su " + maxReps;
holder.progressLabel.setText(newLabel);
}
});
}
});
}
}
public void filterResults(String text) {
text = text == null ? null : text.toLowerCase().trim();
if (text == null || "".equals(text)) {
updateData(mRealm.where(Habit.class).sort("id").findAllAsync());
} else {
updateData(mRealm.where(Habit.class).contains("title", text).sort("id").findAllAsync());
}
}
public Filter getFilter() {
HabitFilter filter = new HabitFilter(this);
return filter;
}
private class HabitFilter extends Filter {
private final HabitCardAdapter adapter;
private HabitFilter(HabitCardAdapter adapter) {
this.adapter = adapter;
}
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
return new FilterResults();
}
@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
adapter.filterResults(charSequence.toString());
}
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView habitTitle;
ProgressBar progressBar;
TextView progressLabel;
ImageButton editHabitButton;
Button checkButton;
public ViewHolder(@NonNull View itemView) {
super(itemView);
habitTitle = itemView.findViewById(R.id.habitCardTitle);
editHabitButton = itemView.findViewById(R.id.counterHabitEditButton);
progressBar = itemView.findViewById(R.id.counterHabitProgressBar);
checkButton = itemView.findViewById(R.id.checkButton);
progressLabel = itemView.findViewById(R.id.counterHabitProgressLabel);
}
void setHabit(Habit habit) {
this.habitTitle.setText(habit.getTitle());
this.progressBar.setProgress(habit.getRepetitions());
this.progressBar.setMax(habit.getMaxRepetitions());
this.progressLabel.setText("Completato " + habit.getRepetitions() + " volte su " + habit.getMaxRepetitions()); //FIXME: sposta la stringa
}
}
}
I don't really know if this is the way to go for this problem, but it's now behaving as expected so I'll share the solution here.
Inside the HabitCardAdapter
I added another OrderedRealmCollection<Habit>
member, called filteredList
, while list
holds the whole data. In the costructor both of filteredList
and list
are tied to the data passed to the constructor, but while filteredList
will be modified by the query, list
will not (probably putting it to final
is the best practice). Then everything in the Adapter
will now reference to filteredList
instead of list
, and when the SearchView
is selected and the query is up, filteredList
will get the data, and then updateData(filteredList)
will be called.
Here is what I changed:
public class HabitCardAdapter extends RealmRecyclerViewAdapter<Habit, HabitCardAdapter.ViewHolder> implements Filterable {
Context ct;
OrderedRealmCollection<Habit> list;
OrderedRealmCollection<Habit> filteredList;
Realm mRealm;
...
}
public HabitCardAdapter(@Nullable OrderedRealmCollection<Habit> data, Context context, Realm realm) {
super(data, true, true);
ct = context;
list = data;
filteredList = data;
mRealm = realm;
setHasStableIds(true);
}
Probably the error was here in getItemCount()
, when the filteredList
size was smaller than the list
one, but since I didn't have any reference to filteredList
, I didn't have any way to change that size, and so the Adapter
would continue to show - for example - 6 views while I was querying for 3. Having it as a properly class member it let me make this:
@Override
public int getItemCount() {
return this.filteredList.size();
}
public void filterResults(String text) {
text = text == null ? null : text.toLowerCase().trim();
if (text == null || "".equals(text)) {
filteredList = list;
} else {
filteredList = mRealm.where(Habit.class).beginsWith("title", text, Case.INSENSITIVE).sort("id").findAll();
}
updateData(filteredList);
}