Search code examples
javaandroidandroid-recyclerviewsearchview

SearchView filter is working as intented, but retrieves old item position on RecyclerView in fragment


I've been searching the past few days for a solution to my issue, but all other similar issues seem to have a problem with an onClickListener or their solution isn't intended for the issue at hand. It seems like the onClickListeners I use in my project are working as intended, only the SearchView is not.

My starting list consists of some items and their ingredients/materials, and everything is working as intented. When filtered with the SearchView, the new filtered list is showing correct item names but wrong ingredients. Literally the old position before the filtering and I can't seem to debug where the problem is coming from.

MainAdapter.java

public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> implements Filterable {
    private List<Item> mItemCard;
    private List<Item> mItemCardFull; //Needed for the getFilter() method

    static class ViewHolder extends RecyclerView.ViewHolder {


        // region Variables (Item Name, Icon, Expand Layout & Expand button)
        ImageView mItemIcon;
        TextView mItemName;
        ImageButton mExpandButton;
        // etc...

        // region Variables (Ingredients' Icons, Names & Amount)
        ImageView mIngredient_1_Icon;
        TextView mIngredient_1_Name;
        TextView mIngredient_1_amountTV;
        float mIngredient_1_amount;
        // etc...

        // region misc Variables
        boolean _isFirstTime = true; // Used in initialization & crafting method switch
        EditText editFactoryAmount;
        ImageView factoryAmountIcon;
        Button factoryAmountButton;
        // etc...


        ViewHolder(@NonNull View itemView) {
            super(itemView);
            mItemIcon = itemView.findViewById(R.id.itemIcon);
            mItemName = itemView.findViewById(R.id.itemName);
            mIngredient_1_Icon = itemView.findViewById(R.id.ingredient_1_icon);
            mIngredient_1_Name = itemView.findViewById(R.id.ingredient_1_name);
            mIngredient_1_amountTV = itemView.findViewById(R.id.ingredient_1_amount);
        // etc...
        }
    }

    public MainAdapter(List<Item> itemCard) {
        this.mItemCard = itemCard;
        this.mItemCardFull = new ArrayList<>(itemCard);
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        final ViewHolder holder = new ViewHolder(v);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
        final Item currentItem = mItemCard.get(position);

        final float craftingTime = currentItem.getCraftingTime();
        final float outputAmount = currentItem.getOutputAmount();

        holder.mItemIcon.setImageResource(currentItem.getItemIcon());
        holder.mItemName.setText(currentItem.getItemName());
        holder.mExpandButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                boolean show = toggleLayout(!currentItem.isExpanded(), view, holder.mExpandedLayout);
                currentItem.setExpanded(show);
            }
        });

        // region Textwatchers
        // region Editor Listeners
        /* Editor Listener: When actionDone is clicked on the soft keyboard, clear focus from editText */

        // other random methods included in the project which arent relevant
        // ......
    }

    /* Expands or collapses depending on item state */
    private boolean toggleLayout(boolean isExpanded, View v, ConstraintLayout expandedLayout) {
        Animations.toggleArrow(v, isExpanded);
        if (isExpanded) {
            Animations.expand(expandedLayout);
        } else {
            Animations.collapse(expandedLayout);
        }
        return isExpanded;
    }

    @Override
    public int getItemViewType(int position) {
        return position;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemCount() {
        return mItemCard.size();
    }

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~ Search Filter Setup ~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    @Override
    public Filter getFilter() {
        return itemFilter;
    }

    private Filter itemFilter = new Filter() {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            List<Item> filteredList = new ArrayList<>();

            if (constraint == null || constraint.length() == 0) {
                filteredList.addAll(mItemCardFull);
            } else {
                String filterPattern = constraint.toString().toLowerCase().trim();

                for (Item item : mItemCardFull) {
                    if (item.getItemName().toLowerCase().contains(filterPattern)) {
                        filteredList.add(item);
                    }
                }
            }

            FilterResults results = new FilterResults();
            results.values = filteredList;
            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            mItemCard.clear();
            mItemCard.addAll((List) results.values);
            notifyDataSetChanged();
        }
    };
}

IntermediatesFragment.java

public class IntermediatesFragment extends Fragment {

    private RecyclerView mRecyclerView;
    private MainAdapter mAdapter;
    private RecyclerView.LayoutManager mLayoutManager;


    public IntermediatesFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View thisFragment = inflater.inflate(R.layout.fragment_layout, container, false);
        setHasOptionsMenu(true);

        mRecyclerView = thisFragment.findViewById(R.id.recyclerView);
        mRecyclerView.setHasFixedSize(true);
        mLayoutManager = new LinearLayoutManager(getContext());


        createItemList();
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setAdapter(mAdapter);
        return thisFragment;
    }

    @Override
    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);

        MenuItem searchItem = menu.findItem(R.id.action_search);
        SearchView searchView = (SearchView) searchItem.getActionView();

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                mAdapter.getFilter().filter(newText);
                return false;
            }
        });
    }

    private void createItemList() {
        ArrayList<Item> itemList = new ArrayList<>();
        // region Intermediates Items
        // etc items ...

        mAdapter = new MainAdapter(itemList);
    }
}

The problem seems to go away if I include "setIsRecyclable(false)" in the Viewholder, but then the performance is awful on low-end phones and the cards are destroyed/resetted when they exit the view which is not a desirable behaviour.

Edit with full onBindViewHolder:

@Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
        final Item currentItem = mItemCard.get(position);

        final float craftingTime = currentItem.getCraftingTime();
        final float outputAmount = currentItem.getOutputAmount();

        holder.mItemIcon.setImageResource(currentItem.getItemIcon());
        holder.mItemName.setText(currentItem.getItemName());
        holder.mExpandButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                boolean show = toggleLayout(!currentItem.isExpanded(), view, holder.mExpandedLayout);
                currentItem.setExpanded(show);
            }
        });

        // region Ingredients' Icons, Names & Amounts
        holder.mIngredient_1_Icon.setImageResource(currentItem.getIngredientIcon_1());
        holder.mIngredient_2_Icon.setImageResource(currentItem.getIngredientIcon_2());
        holder.mIngredient_3_Icon.setImageResource(currentItem.getIngredientIcon_3());
        holder.mIngredient_4_Icon.setImageResource(currentItem.getIngredientIcon_4());
        holder.mIngredient_5_Icon.setImageResource(currentItem.getIngredientIcon_5());
        holder.mIngredient_6_Icon.setImageResource(currentItem.getIngredientIcon_6());

        holder.mIngredient_1_Name.setText(currentItem.getIngredientName_1());
        holder.mIngredient_2_Name.setText(currentItem.getIngredientName_2());
        holder.mIngredient_3_Name.setText(currentItem.getIngredientName_3());
        holder.mIngredient_4_Name.setText(currentItem.getIngredientName_4());
        holder.mIngredient_5_Name.setText(currentItem.getIngredientName_5());
        holder.mIngredient_6_Name.setText(currentItem.getIngredientName_6());

        holder.mIngredient_1_amountTV.setText(String.valueOf(currentItem.getIngredientAmount_1()));
        holder.mIngredient_2_amountTV.setText(String.valueOf(currentItem.getIngredientAmount_2()));
        holder.mIngredient_3_amountTV.setText(String.valueOf(currentItem.getIngredientAmount_3()));
        holder.mIngredient_4_amountTV.setText(String.valueOf(currentItem.getIngredientAmount_4()));
        holder.mIngredient_5_amountTV.setText(String.valueOf(currentItem.getIngredientAmount_5()));
        holder.mIngredient_6_amountTV.setText(String.valueOf(currentItem.getIngredientAmount_6()));


        if (currentItem.getIngredientName_1() == R.string.item_null) {
            holder.mIngredient_1_Icon.setVisibility(View.GONE);
            holder.mIngredient_1_Name.setVisibility(View.GONE);
            holder.mIngredient_1_amountTV.setVisibility(View.GONE);\

        // if statements continue for the rest of the 5 ingredients
        // endregion Ingredients' Icons, Names & Amounts

        holder.factoryAmountButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                holder.button_f++;
                float factoryAmount = Float.parseFloat(holder.editFactoryAmount.getText().toString());
                float outputSpeed = Float.parseFloat((holder.editItemOutput.getText().toString()));
                if (currentItem.getCraftingMethod().equals("Assembler")) {
                    switch (holder.button_f) {
                        case 0:
                            holder.factoryAmountIcon.setImageResource(R.drawable.pf_assembling_machine_1);
                            holder.craftingSpeed = 0.50f;
                            if (holder.editItemOutput.isFocused()) {
                                holder.factoryAmount = (outputSpeed * craftingTime) / ((outputAmount * holder.craftingSpeed) * (1 + (holder.speedModuleAmount * holder.speed)) * (1 + (holder.prodModuleAmount * holder.productivity)));
                                holder.editFactoryAmount.setText(String.valueOf(holder.factoryAmount));
                                // region Edit Ingredients' Amount
                                holder.factoryAmount = Float.parseFloat(holder.editFactoryAmount.getText().toString());
                                holder.mIngredient_1_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_1()) / craftingTime;
                                holder.mIngredient_1_amountTV.setText(String.valueOf(holder.mIngredient_1_amount));

                                holder.mIngredient_2_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_2()) / craftingTime;
                                holder.mIngredient_2_amountTV.setText(String.valueOf(holder.mIngredient_2_amount));

                                holder.mIngredient_3_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_3()) / craftingTime;
                                holder.mIngredient_3_amountTV.setText(String.valueOf(holder.mIngredient_3_amount));

                                holder.mIngredient_4_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_4()) / craftingTime;
                                holder.mIngredient_4_amountTV.setText(String.valueOf(holder.mIngredient_4_amount));

                                holder.mIngredient_5_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_5()) / craftingTime;
                                holder.mIngredient_5_amountTV.setText(String.valueOf(holder.mIngredient_5_amount));

                                holder.mIngredient_6_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_6()) / craftingTime;
                                holder.mIngredient_6_amountTV.setText(String.valueOf(holder.mIngredient_6_amount));


            // it goes on in the same pattern depending on other circumstances


        /*~~~~~~~~~~~~ Initialization & Crafting Method Switch ~~~~~~~~~~~~~~~~~~*/
        if (holder.editFactoryAmount.getText().toString().isEmpty() && !holder.editFactoryAmount.isFocused()) {
            holder.editFactoryAmount.setText(String.valueOf(1.0f));
            holder.outputSpeed = ((Float.parseFloat(holder.editFactoryAmount.getText().toString()) * holder.craftingSpeed) * (1 + (holder.speedModuleAmount * holder.speed))) / craftingTime * (1 + (holder.prodModuleAmount * holder.productivity)) * outputAmount;
            holder.editItemOutput.setText(String.valueOf(holder.outputSpeed));
        }

        float factoryAmount = Float.parseFloat(holder.editFactoryAmount.getText().toString());
        switch (currentItem.getCraftingMethod()) {
            case "Assembler":
                // If it's the first time the app starts, set default crafting speed, else keep as it is.
                if (holder._isFirstTime) {
                    holder.craftingSpeed = 0.50f;
                    holder._isFirstTime = false;
                }
                if (holder.craftingSpeed == 0.50f) {
                    holder.factoryAmountIcon.setImageResource(R.drawable.pf_assembling_machine_1);
                    holder.button_f = 0;
                } else if (holder.craftingSpeed == 0.75f) {
                    holder.factoryAmountIcon.setImageResource(R.drawable.pf_assembling_machine_2);
                    holder.button_f = 1;
                } else {
                    holder.factoryAmountIcon.setImageResource(R.drawable.pf_assembling_machine_3);
                }
                holder.outputSpeed = ((factoryAmount * holder.craftingSpeed) * (1 + (holder.speedModuleAmount * holder.speed))) / craftingTime * (1 + (holder.prodModuleAmount * holder.productivity)) * outputAmount;
                holder.editItemOutput.setText(String.valueOf(holder.outputSpeed));
                // region Edit Ingredients' Amount
                holder.factoryAmount = Float.parseFloat(holder.editFactoryAmount.getText().toString());
                holder.mIngredient_1_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_1()) / craftingTime;
                holder.mIngredient_1_amountTV.setText(String.valueOf(holder.mIngredient_1_amount));

                holder.mIngredient_2_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_2()) / craftingTime;
                holder.mIngredient_2_amountTV.setText(String.valueOf(holder.mIngredient_2_amount));

                holder.mIngredient_3_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_3()) / craftingTime;
                holder.mIngredient_3_amountTV.setText(String.valueOf(holder.mIngredient_3_amount));

                holder.mIngredient_4_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_4()) / craftingTime;
                holder.mIngredient_4_amountTV.setText(String.valueOf(holder.mIngredient_4_amount));

                holder.mIngredient_5_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_5()) / craftingTime;
                holder.mIngredient_5_amountTV.setText(String.valueOf(holder.mIngredient_5_amount));

                holder.mIngredient_6_amount = (holder.factoryAmount * holder.craftingSpeed * currentItem.getIngredientAmount_6()) / craftingTime;
                holder.mIngredient_6_amountTV.setText(String.valueOf(holder.mIngredient_6_amount));
                // endregion Edit Ingredients' Amount
                break;
            case "Smelter":
            // thing happening here and etc for all the other cases
        }
    }

Solution

  • It's hard to tell exactly what's wrong here without the full picture, but...

    You're holding a lot of state in your ViewHolder, like whether it's the first time, the ingredient amounts, etc. You really want to only hold Views there, because they can be swapped unpredictably between items. That's why it works when you tell it they're not recyclable - it doesn't do that any more.

    You're also setting things to GONE, but not VISIBLE if they should be shown. This is a problem for recycling:

    holder.mIngredient_1_Icon.setVisibility(View.GONE);
    

    Your button onClick listener is modifying state in the holder - that's bad, because the holders will get swapped between items, and maybe rebound. You really want allllll this state in your Item class. Once you do that, and your RecyclerView becomes a one-way transformation from Items to an onBindViewHolder that sets up everything, it'll be a lot easier to reason about.