Search code examples
androidsortingandroid-recyclerviewandroid-roomcardview

Android Recycler+CardView with Icon and Spinner - Sorting Problem


So I am having a bigger problem right now with my current application.

First let me wrap up the overall setting:

I am using Room for my Database and my Entity is called Sub - one column is about "numUsage" (the number of usages). The UI of the Activity is a RecyclerView with a CardView - better said it is two cards plus one heart icon per row.

  • First Card: Shows name of Sub
  • Second Card: Shows number of usages of Sub
  • Heart Icon: If this Icon is clicked it should change into a filled heart (like Instagram) and count up the usage point of its Sub (the idea is also to make it only clickable once per day put that is a future problem)

On top I have a spinner to give the User the possibility to "sort" the list he gets. He can sort it after "entry" (in order of how the Subs got entered into the DB), "top" (numUsage DESC) and "flop" (numUsage ASC)

The sorting itself happens via the DAO right out of the Database and however the spinner's choice is, the Activity calls for the "fitting" DAO (shows in the Code below).

The sorting works. Also the fact works that whenever I hit the Heart Icon of a Sub, the numUsage counts up one and the Entity is updated.

Now we get to the trouble: When I hit the heart icon it changes (like I want) - but when I now set the spinner on another "sorting way" (f.e. from "entry" to "top") the two CardViews update/sort but the heart icon stays at the very same position (but it should move as well because I want to see which Sub already got a count up)

I know this is because I do not actually sort the Cards but just the way I take the Data out of the Database and am writing the Cards new (so the heart icon stays at its original position). But I am just missing the right idea how I could fix this.

Would be awesome if you could help me out!


Here is a small picture to illustrate my problem:

enter image description here


SubDAO:

//Get all subs
@Query("SELECT * FROM subscriptions")
LiveData<List<Sub>> getAllSubs();

//Get sub with less used points (=worst/flop Sub)
@Query("SELECT * FROM subscriptions ORDER BY numUsage ASC")
LiveData<List<Sub>> getAllFlopSubs();

//Get sub with most used points (=top Sub)
@Query("SELECT * FROM subscriptions ORDER BY numUsage DESC")
LiveData<List<Sub>> getAllTopSubs();

UsageActivity - what happens with the spinner:

@Override
public void onItemSelected(AdapterView<?> parent, View v, int position, long id){
    switch (position){
        case 0:
            ((TextView) parent.getChildAt(0)).setTextColor(getResources().getColor(R.color.midBlue));
            setupListViewEntry();
            break;
        case 1:
            ((TextView) parent.getChildAt(0)).setTextColor(getResources().getColor(R.color.midBlue));
            setupListViewTop();
            break;
        case 2:
            ((TextView) parent.getChildAt(0)).setTextColor(getResources().getColor(R.color.midBlue));
            setupListViewFlop();
            break;

    }
}


@Override
public void onNothingSelected(AdapterView<?> arg0){
    setupListViewEntry();
}






private void setupListViewEntry() {
    mSubViewModel.getAllSubs().observe(this, new Observer<List<Sub>>() {
        @Override
        public void onChanged(@Nullable List<Sub> subs) {
            mAdapter.setSubs(subs);

        }
    });
}






private void setupListViewTop() {
    mSubViewModel.getAllTopSubs().observe(this, new Observer<List<Sub>>() {
        @Override
        public void onChanged(@Nullable List<Sub> subs) {
            mAdapter.setSubs(subs);

        }
    });
}






private void setupListViewFlop() {
    mSubViewModel.getAllFlopSubs().observe(this, new Observer<List<Sub>>() {
        @Override
        public void onChanged(@Nullable List<Sub> subs) {
            mAdapter.setSubs(subs);

        }
    });
}

UsageAdapter:

public class UsageSubAdapter extends RecyclerView.Adapter{

List<Sub> subList;
Context context;
private final LayoutInflater mInflater;

UsageSubAdapter (Context context) {
    mInflater = LayoutInflater.from(context);
}


public class ViewHolder extends  RecyclerView.ViewHolder {
    public TextView subUsage_name_cv_dummy;
    public TextView subUsage_times_cv_dummy;
    public View clickable;
    public ImageView plus_usage;
    public ViewHolder(View itemView) {
        super(itemView);
        subUsage_name_cv_dummy = itemView.findViewById(R.id.usage_name_dummy);
        subUsage_times_cv_dummy = itemView.findViewById(R.id.usage_times_dummy);
        clickable = itemView.findViewById(R.id.usage_cv_left);
        plus_usage = itemView.findViewById(R.id.usage_like_btn);
    }
}

@Override
public UsageSubAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
    View subview = mInflater.inflate(R.layout.cv_usage, parent, false);

    return new UsageSubAdapter.ViewHolder(subview);
}


@Override
public void onBindViewHolder (final UsageSubAdapter.ViewHolder holder, int position) {

    //Set Texts for CardViews
    if (subList!= null){
        final Sub current = subList.get(position);

        holder.subUsage_name_cv_dummy.setText(current.getSubName());
        holder.subUsage_times_cv_dummy.setText(Integer.toString(current.getNumUsage()));

        holder.clickable.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Click on left CV to open up InfoFragment
                ShowInfoFragment currentFrag = ShowInfoFragment.newInstance(current);
                UsageActivity.fragmentManager.beginTransaction().replace(R.id.compLayout, currentFrag).addToBackStack(null).commit();
                Log.d("Clicking: ", current.getSubName() + " was clicked - Listener worked");
            }
        });

        holder.plus_usage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Count up usage point
                int currentUsage = current.getNumUsage();
                int newUsage = currentUsage + 1;
                current.setNumUsage(newUsage);
                updateNumUsage(current);

                //change icon so the user knows it was clicked (for today)
                holder.plus_usage.setImageResource(R.drawable.ic_usage_point_given);
                //make it only clickable once a day

            }
        });
    }
    else {
        holder.subUsage_name_cv_dummy.setText("Create a new Sub!");
        holder.subUsage_times_cv_dummy.setText("00");
    }

}



//Updating the number of Usages
private void updateNumUsage (final Sub sub){
    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            SubDatabase.getInstance(context)
                    .getsubDAO()
                    .updateSub(sub);
        }
    }).start();
}




public void setSubs(List<Sub> subs){
    this.subList = subs;
    notifyDataSetChanged();
}



@Override
public int getItemCount(){
    if(subList!=null){
        return subList.size();
    } else
    {
        return 0;
    }
}

}


Solution

  • The issue is that you're currently not storing the value of the heart icon. And without this information, you cannot tell the RecyclerView how to render it.

    Looking at your onBindViewHolder, you're updating the value for the name, and the count, but not the heart. You only set the heart to active when you're clicking it.

    final Sub current = subList.get(position);
    
    holder.subUsage_name_cv_dummy.setText(current.getSubName());
    holder.subUsage_times_cv_dummy.setText(Integer.toString(current.getNumUsage()));
    
    // TODO - Set the current state of the heart icon.
    
    holder.clickable.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
           // ...
        }
    });
    holder.plus_usage.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            holder.plus_usage.setImageResource(R.drawable.ic_usage_point_given);
        }
    });
    

    What you need to do is save a value, and use that when rendering it. For this example, let's assume you've added boolean isActive = false; to your Sub class.

    First, since we need to update the icon in two places ( rendering, and onClick ), I'd create a small function like this.

    void updateUsageIcon(UsageSubAdapter.ViewHolder holder, Sub sub) {
        if( sub.isActive ) {
            holder.plus_usage.setImageResource(R.drawable.ic_usage_point_given);
        } else {
            // TODO show inactive state
        }
    }
    

    And then, in your ViewHolder, you can do something like this.

    final Sub current = subList.get(position);
    
    holder.subUsage_name_cv_dummy.setText(current.getSubName());
    holder.subUsage_times_cv_dummy.setText(Integer.toString(current.getNumUsage()));
    
    // Set the current value.
    updateUsageIcon(holder, sub);
    
    holder.plus_usage.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
    
            // Add logic to check if the user is allowed to set this to true today.
            sub.isActive = true;
    
            // Update the current value.
            updateUsageIcon(holder, sub);
        }
    });