Secondary sort on ArrayAdapter - android

i am trying to sort my Arrayadapter with it's sort-Method
#Override
public void sort(#NonNull Comparator<? super Task> comparator) {
super.sort(comparator);
}
two times.
I'm using the two Comparators
private static final Comparator TASKCOMPARATOR_TITLE = new Comparator<Task>() {
#Override
public int compare(Task a, Task b) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
}
};
private static final Comparator TASKHOLDERCOMPARATOR_DUEDATE = new Comparator<ViewHolder>() {
#Override
public int compare(ViewHolder a, ViewHolder b) {
return a.getTask().getDueTime().compareTo(b.getTask().getDueTime());
}
};
like this
taskAdapter.sort(Util.getTASKCOMPARATOR_TITLE());
taskAdapter.sort(Util.getTASKCOMPARATOR_DUEDATE());
hoping to secondary sort the ArrayAdapter by the title and then by the Date. The sort-Method of the ArrayAdapter is internally using
public void sort(Comparator<? super T> comparator) {
synchronized (mLock) {
if (mOriginalValues != null) {
Collections.sort(mOriginalValues, comparator);
} else {
Collections.sort(mObjects, comparator);
}
}
if (mNotifyOnChange) notifyDataSetChanged();
}
I've read, that Collections.sort() is using a stable algorithm and therefore i'm wondering why the List of my ArrayAdapter is just being sorted by the last comparator i call.
Can anyone tell me where i made a mistake and why the first sort()-call is being ignored by the second one?
EDIT
private static final Comparator TASKCOMPARATOR = new Comparator<Task>() {
#Override
public int compare(Task a, Task b) {
int timeCompareResult = a.getDueTime().compareTo(b.getDueTime());
if (timeCompareResult == 0) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
} else {
return timeCompareResult;
}
}
};
This works. I don't know if it's the only/best way.

As Шах stated, use one comparator.
Comparator returns 0 on equals.
First compare on Title, if they are equal you go for the comparison on Time.
I haven't ran the code, but it should be something like this:
private static final Comparator TASKHOLDERCOMPARATOR_DUEDATE = new Comparator<ViewHolder>() {
#Override
public int compare(ViewHolder a, ViewHolder b) {
int comparison = a.getTask().getTitle().compareToIgnoreCase(b.getTask().getTitle());
if(comparison == 0){
comparison = a.getTask().getDueTime().compareTo(b.getTask().getDueTime());
}
return comparison;
}
};

Related

Android how to sort RecyclerView List when using AsyncListDiffer?

I have a RecyclerView that shows a list of CardViews. I recently switched the project from using RecyclerView Adapter to using an AsyncListDiffer Adapter to take advantage of adapter updates on a background thread. I have converted over all previous CRUD and filter methods for the list but cannot get the sort method working.
I have different types or categories of CardViews and I would like to sort by the types/categories. I clone the existing list mCards so the "behind the scenes" DiffUtil will see it as a different list, as compared to the existing list that I wanted to sort. And then I use AsynListDiffer's submitList().
The list is not sorting. What am I missing here?
MainActivity:
private static List<Card> mCards = null;
...
mCardViewModel = new ViewModelProvider(this).get(CardViewModel.class);
mCardViewModel.getAllCards().observe(this,(cards -> {
mCards = cards;
cardsAdapter.submitList(mCards);
}));
mRecyclerView.setAdapter(cardsAdapter);
A click on a "Sort" TextView runs the following code:
ArrayList<Card> sortItems = new ArrayList<>();
for (Card card : mCards) {
sortItems.add(card.clone());
}
Collections.sort(sortItems, new Comparator<Card>() {
#Override
public int compare(Card cardFirst, Card cardSecond) {
return cardFirst.getType().compareTo(cardSecond.getType());
}
});
cardsAdapter.submitList(sortItems);
// mRecyclerView.setAdapter(cardsAdapter); // Adding this did not help
AsyncListDifferAdapter:
public AsyncListDifferAdapter(Context context) {
this.mListItems = new AsyncListDiffer<>(this, DIFF_CALLBACK);
this.mContext = context;
this.mInflater = LayoutInflater.from(mContext);
}
public void submitList(List<Quickcard> list) {
if (list != null) {
mListItems.submitList(list);
}
}
public static final DiffUtil.ItemCallback<Card> DIFF_CALLBACK
= new DiffUtil.ItemCallback<Card>() {
#Override
public boolean areItemsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldItem.getId() == newItem.getId();
}
#Override
public boolean areContentsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
return oldItem.equals(newItem);
}
#Nullable
#Override
public Object getChangePayload(#NonNull Card oldItem, #NonNull Card newItem) {
return super.getChangePayload(oldItem, newItem);
}
};
Model:
#Entity(tableName = "cards")
public class Card implements Parcelable, Cloneable {
// Parcelable code not shown for brevity
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "cardId")
public int id;
#ColumnInfo(name = "cardType")
private String type;
#Ignore
public Card(int id, String type) {
this.id = id;
this.type = type;
}
public int getId() {
return this.id;
}
public String getType() {
return this.type;
}
#Override
public boolean equals(Object obj) {
if (obj == this)
return true;
else if (obj instanceof Card) {
Card card = (Card) obj;
return id == card.getId() &&
type.equals(card.getType());
} else {
return false;
}
}
#NonNull
#Override
public Card clone() {
Card clone;
try {
clone = (Card) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return clone;
}
Instead of using notifyDataSetChanged() we can use notifyItemMoved(). That solution gives us a nice animation of sorting. I put the sort order within the adapter. We need a displayOrderList that will contain the currently displayed elements because mDiffer.getCurrentList() doesn't change the order of elements after notifyItemMoved(). We first moved the element that is first sorted to the first place, the second sorted element to second place,... So inside the adapter put the following:
public void sortByType()
{
List<Card> sortedList = new ArrayList<>(mDiffer.getCurrentList());
sortedList.sort(Comparator.comparing(Card::getType));
List<Card> displayOrderList = new ArrayList<>(mDiffer.getCurrentList());
for (int i = 0; i < sortedList.size(); ++i)
{
int toPos = sortedList.indexOf(displayOrderList.get(i));
notifyItemMoved(i, toPos);
listMoveTo(displayOrderList, i, toPos);
}
}
private void listMoveTo(List<Card> list, int fromPos, int toPos)
{
Card fromValue = list.get(fromPos);
int delta = fromPos < toPos ? 1 : -1;
for (int i = fromPos; i != toPos; i += delta) {
list.set(i, list.get(i + delta));
}
list.set(toPos, fromValue);
}
and then call from activity cardsAdapter.sortByType();
I think issue is in below method
public void submitList(List<Quickcard> list) {
if (list != null) {
mListItems.submitList(list);
}
}
because here first you need to clear old arraylist "mListItems" using
mListItems.clear();
//then add new data
if (list != null) {
mListItems.addAll(list);
}
//now notify adapter
notifyDataSetChanged();
Or Also you can direct notify adapter after sorting.
First set adapter and pass your main list in adapter's constructor
Collections.sort(sortItems, new Comparator<Card>() {
#Override
public int compare(Card cardFirst, Card cardSecond) {
return cardFirst.getType().compareTo(cardSecond.getType());
}
});
//now direct notify adpter
your_adapter_object.notifyDataSetChanged();
I clone the existing list mCards so the "behind the scenes" DiffUtil will see it as a different list
DiffUtil will detect changes by your implementation of DiffUtil.ItemCallback<Card>, so when you clone a card you just create a new instance of it with the same ID, therefor DiffUtil sees it as the same item because you are checking if the items are the same based on their ID:
#Override
public boolean areItemsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldItem.getId() == newItem.getId();
}
but because the cloned object's reference is different from the original item, DiffUitl will detect the items content change, because of:
#Override
public boolean areContentsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
return oldItem.equals(newItem);
}
so DiffUtil will call adapter.notifyItemChanged(updateIndex) and will update the viewHolder in that index, but it won't change the order of items in the recyclerView.
There seems to be another problem in your sort code. You say
I would like to sort by the types/categories
but you are sorting your list alphabetically every time you click the textView. This code will give you same result every time you run it and won't change the order of recyclerView assuming the original list is sorted in the first hand. If the original list isn't sorted, clicking the textView will change the order just once.

How to change the data of an item at once using DiffUtil?

When I press the toggle button, I want to change the units of the list of the currently displayed recycler views at once.
I used ListAdapter + DiffUtil to display the recycler view.
The way I tried to implement this feature is to load the current list when the toggle button is pressed.
Then, after resetting the new toggle unit values ​​for the current lists, I used submitList() to update the list.
But this was the wrong way.
My guess is because the variable created for the value of the list loaded to be updated has the same reference value, so the value changed at the same time.
In other words, there is no change because the values ​​of the update list and the existing list are the same.
What can I do to solve this problem?
RoutineDetailModel.java
public class RoutineDetailModel {
public int id;
private int set = 1;
public static String unit = "kg";
public RoutineDetailModel() {
Random random = new Random();
this.id = random.nextInt();
}
public RoutineDetailModel(int set) {
Random random = new Random();
this.id = random.nextInt();
this.set = set+1;
}
public int getSet() {
return set;
}
public int getId() {
return id;
}
public String getWeight() {
return weight;
}
public String getUnit() {
return unit;
}
#Override
public int hashCode() {
return Objects.hash(set, weight); // getWeight를 호출하면 더 다양하게 되나?
}
#Override
public boolean equals(#Nullable Object obj) {
if(obj != null && obj instanceof RoutineDetailModel) {
RoutineDetailModel model = (RoutineDetailModel) obj;
if(this.id == model.getId()) {
return true;
}
}
return false;
}
}
MainActivity.java
public class WriteRoutineActivity extends AppCompatActivity implements WritingCommentDialogFragment.OnDialogClosedListener {
List<RoutineModel> items;
RoutineListAdapter listAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_write_routine);
listAdapter.setOnRoutineClickListener(new RoutineListAdapter.OnRoutineItemClickListener() {
#Override
public void onUnitBtnClicked(int curRoutinePos, String unit) {
Object obj = listAdapter.getRoutineItem(curRoutinePos);
RoutineModel item = (RoutineModel) obj;
if(obj instanceof RoutineModel) {
for(RoutineDetailModel detailItem : item.getDetailItemList()) {
detailItem.setUnit(unit);
}
listAdapter.submitList(getUpdatedList());
}
}
});
}
}
Please tell me if you need more information
To change all the items you need to use notifyDataSetChanged() otherwise you cannot update ALL the items.
In order to do so, you must create a method inside your adapter which does the following ->
public void updateItems(String unit) {
for({YOUR ITEM TYPE} item: {YOUR LIST}) {
item.unit = unit;
}
notifyDataSetChanged();
}
And call this method when you want to change all units.
yourAdapter.updateItems("Kg");

How to pass ArrayList that contains Base64 String to another activity from recyclerview

I am working on a project where i need to create Image Preview Functionality.For that i have created a recyclerview in which i am passing ArrayList of bitmap and displaying it in recyclerview.Now i am converting that arraylist into base64 string array and want to pass that arraylist into new activity using parcelable.
But i am getting TransactionTooLarge Execption.
Is there another way to pass the array to another activity?
Here is my adapter
public class ImageListAdapter extends RecyclerView.Adapter<ImageListAdapter.ViewHolder> {
private ArrayList<UploadImageModel> mBitmapArray;
private Context context;
private UploadImageModel mUploadImageModel;
private ArrayList<Base64ArrayModel> mBase64ArrayList;
private Base64ArrayModel mBase64ArrayModel;
public ImageListAdapter(ArrayList<UploadImageModel> mBitmapArray, ArrayList<Base64ArrayModel> mBase64ArrayList, Context context) {
this.mBitmapArray = mBitmapArray; //Here i am getting arraylist that contains bitmaps
this.context = context;
}
#Override
public ImageListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(context).inflate(R.layout.image_set, null);
ViewHolder holder = new ViewHolder(itemView);
return holder;
}
#Override
public void onBindViewHolder(ImageListAdapter.ViewHolder holder, int position) {
mUploadImageModel = mBitmapArray.get(position);
holder.UploadImageView.setImageBitmap(mUploadImageModel.getUploadImageBitmap());
holder.UploadImageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent openPreviewActivity = new Intent(context, PreviewActivity.class);
openPreviewActivity.putParcelableArrayListExtra("myImageList",encodeList());
context.startActivity(openPreviewActivity);
}
});
}
#Override
public int getItemCount() {
return mBitmapArray.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public ImageView UploadImageView;
public ViewHolder(View itemView) {
super(itemView);
UploadImageView = (ImageView) itemView.findViewById(R.id.UploadImageView);
}
}
private ArrayList<Base64ArrayModel> encodeList() {
mBase64ArrayList = new ArrayList<>();
for (int i = 0; i < mBitmapArray.size(); i++) {
mBase64ArrayList.add(new Base64ArrayModel(ConstantFunction.encodeToBase64(mBitmapArray.get(i).getUploadImageBitmap(), Bitmap.CompressFormat.JPEG, 100)));
}
return mBase64ArrayList;
}
}
and the model i am using is as follows
public class Base64ArrayModel implements Parcelable {
public String mBase64BitmapString;
public String getmBase64BitmapString() {
return mBase64BitmapString;
}
public void setmBase64BitmapString(String mBase64BitmapString) {
this.mBase64BitmapString = mBase64BitmapString;
}
public Base64ArrayModel(String mBase64BitmapString)
{
this.mBase64BitmapString=mBase64BitmapString;
}
protected Base64ArrayModel(Parcel in) {
mBase64BitmapString = in.readString();
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mBase64BitmapString);
}
#SuppressWarnings("unused")
public static final Parcelable.Creator<Base64ArrayModel> CREATOR = new Parcelable.Creator<Base64ArrayModel>() {
#Override
public Base64ArrayModel createFromParcel(Parcel in) {
return new Base64ArrayModel(in);
}
#Override
public Base64ArrayModel[] newArray(int size) {
return new Base64ArrayModel[size];
}
};
}
How can i pass that arrayList to new activity?
From the doc,
During a remote procedure call, the arguments and the return value of
the call are transferred as Parcel objects stored in the Binder
transaction buffer. If the arguments or the return value are too large
to fit in the transaction buffer, then the call will fail and
TransactionTooLargeException will be thrown.
The Binder transaction buffer has a limited fixed size, currently 1Mb,
which is shared by all transactions in progress for the process.
Consequently this exception can be thrown when there are many
transactions in progress even when most of the individual transactions
are of moderate size.
So, this basically means, you're trying to pass data with a size greater than the Binder Transaction Buffer can contain. To overcome this, you've to reduce the size of the data(base64String size, for your case). I can see you've this
ConstantFunction.encodeToBase64(mBitmapArray.get(i).getUploadImageBitmap(), Bitmap.CompressFormat.JPEG, 100) method for encoding a bitmap to base64String where you've passed 100 as compression level. In your implementation, if you use bitmap.compress method to compress the bitmap then try to reduce the number. The less the number the less quality it would get after the compression hence, you'll get small sized base64String in the end.
first, you add this line into your manifest file.
android:largeHeap="true"
Because simultaneously at a time your transaction too large. So make one singleton class like. It is not preferred way I want to suggest use database but if you have not any other choice than this one is better for you.
public class DataTransactionModel {
private static volatile DataTransactionModel instance = null;
private ArrayList<Base64ArrayModel> list = null;
private DataTransactionModel() {
}
public static synchronized DataTransactionModel getInstance() {
if (instance == null) {
synchronized (DataTransactionModel.class) {
if (instance == null) {
instance = new DataTransactionModel();
}
}
}
return instance;
}
public Bitmap getList() {
return list;
}
public void setList(Bitmap bitmap) {
this.bitmap = list;
}
}
Set data into this singleton class and then after get list of images with the help of singleton class methods.
...
#Override
public void onBindViewHolder(ImageListAdapter.ViewHolder holder, int position) {
mUploadImageModel = mBitmapArray.get(position);
holder.UploadImageView.setImageBitmap(mUploadImageModel.getUploadImageBitmap());
holder.UploadImageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
DataTransactionModel model = DataTransactionModel.getInstance();
model.setList(encodeList());
Intent openPreviewActivity = new Intent(context, PreviewActivity.class);
context.startActivity(openPreviewActivity);
}
});
}
You can use EventBus :
Create event and then post the arraylist and receive wherever you want to..
for E.g
compile 'org.greenrobot:eventbus:3.0.0'
//create class
public class Base64Event {
public final List< Base64ArrayModel > base64Array;
public Base64Event(List< Base64ArrayModel > base64Array){
}
}
//Post
EventBus.getDefault().postSticky(new Base64Event(base64Array));
//Reciever in activity
#Subscribe(threadMode = ThreadMode.MAIN)
public void anyName(Base64Event event) {
event. base64Array //here is the data passed
}

How to refresh recyclerview using MVP Structure in android?

I want simple example for MVP structure in android to refresh recyclerview item not the whole list of recyclerview.
It will refresh only items in recyclerview of android.
This is a problem I've thought about quite a lot. There are two possible ways of doing it:
Pass the new List of data to the Adapter and it does the work of working out what's changed and updating the correct items.
Keep a record of the current items in your model then when the new list is calculated send ListChangeItems to the Adapter.
I'll outline both in more detail below. In both cases you need to calculate the differences between what is currently showing and the new data. I have a helper class ListDiffHelper<T> which does this comparison:
public class ListDiffHelper<T> {
private List<T> oldList;
private List<T> newList;
private List<Integer> inserted = new ArrayList<>();
private List<Integer> removed = new ArrayList<>();
public List<Integer> getInserted() {return inserted;}
public List<Integer> getRemoved() {return removed;}
public ListDiffHelper(List<T> oldList, List<T> newList) {
this.oldList = oldList;
this.newList = newList;
checkForNull();
findInserted();
findRemoved();
}
private void checkForNull() {
if (oldList == null) oldList = Collections.emptyList();
if (newList == null) newList = Collections.emptyList();
}
private void findInserted() {
Set<T> newSet = new HashSet<>(newList);
newSet.removeAll(new HashSet<>(oldList));
for (T item : newSet) {
inserted.add(newList.indexOf(item));
}
Collections.sort(inserted, new Comparator<Integer>() {
#Override
public int compare(Integer lhs, Integer rhs) {
return lhs - rhs;
}
});
}
private void findRemoved() {
Set<T> oldSet = new HashSet<>(oldList);
oldSet.removeAll(new HashSet<>(newList));
for (T item : oldSet) {
removed.add(oldList.indexOf(item));
}
Collections.sort(inserted, new Comparator<Integer>() {
#Override
public int compare(Integer lhs, Integer rhs) {
return rhs - lhs;
}
});
}
}
For this to work properly you need to ensure that the equals() method of the Data class compares things in a suitable way.
Adapter Lead
In this case your Presenter calls getData() on the model (or subscribes to it if you're using Rx) and receives List<Data>. It then passes this List to the view through a setData(data) method which in turn give the list to the Adapter. The method in the Adapter would look something like:
private void setData(List<Data> data) {
if (this.data == null || this.data.isEmpty() || data.isEmpty()) {
this.data = data;
adapter.notifyDataSetChanged();
return;
}
ListDiffHelper<Data> diff = new ListDiffHelper<>(this.data, data);
this.data = data;
for (Integer index : diff.getRemoved()) {
notifyItemRemoved(index);
}
for (Integer index : diff.getInserted()) {
notifyItemInserted(index);
}
}
It is important to remove items first before adding new ones otherwise the order will not be maintained correctly.
Model Lead
The alternative approach is to keep the Adapter much dumber and do the calculation of what has changed in your model layer. You then need a wrapper class to send the individual changes to your View/Adapter. Something like:
public class ListChangeItem {
private static final int INSERTED = 0;
private static final int REMOVED = 1;
private int type;
private int position;
private Data data;
public ListChangeItem(int type, int position, Data data) {
this.type = type;
this.position = position;
this.data = data;
}
public int getType() {return type;}
public int getPosition() {return position;}
public Data getData() {return data;}
}
You would then pass a List of these to your Adapter via the view interface. Again it would be important to have the removals actioned before the inserts to ensure the data is in the correct order.

Listview Empty View is always shown

I use a custom ParseQueryAdapter to load data in listview. I want to show a message when there is no data but the message is shown even when data are not empty. I think it is due to the fact that data are not yet loaded. I tried with setEmptyView and also with a test on the adapter if mAdapter.isEmpty().
I tried waiting a few seconds before testing if adapter is empty but although it works, I think it's not a good practice.
My custom adapter where I make the query:
public class CategoryEventsAdapter extends ParseQueryAdapter<Event> {
public CategoryEventsAdapter(Context context, final String c) {
super(context, new ParseQueryAdapter.QueryFactory<Event>() {
public ParseQuery<Event> create() {
ParseQuery<Event> query = new ParseQuery<Event>("Event");
query.whereEqualTo("published", true);
query.whereEqualTo("category", c);
return query;
}
});
}
#Override
public View getItemView(Event event, View v, ViewGroup parent) {
...
}
}
And I simply call it in a Fragment:
mAdapter = new CategoryEventsAdapter(getActivity(), category);
listview.setAdapter(mAdapter);
if (mAdapter.isEmpty()) {
// show message
}
I've never used this particular part of Parse but looking at the docs, the query seems to be async. Instead you can try this maybe:
mAdapter = new CategoryEventsAdapter(getActivity(), category);
// add a listener for when the query is done.
mAdapter.addOnQueryLoadListener(new OnQueryLoadListener<ParseObject>() {
public void onLoaded(List<ParseObject> objects, ParseException e) {
// Check if empty here and show message.
if (objects.size == 0){
// show message
}
}
});
listview.setAdapter(mAdapter);
So once the query is done, it should call onLoaded so then you can determine if it is empty or not. In onLoaded you can check the count of the objects parameter. Not sure if it's already set in the adapter if you do mAdapter.isEmpty at that point.
This is my ParseQueryAdapter implementation that lets you choose an "empty" placeholder. You just call adapter.setEmptyLayoutId(R.layout.empty) from outside.
public class ParseAdapter extends ParseQueryAdapter {
private final static int EMPTY_VIEW = 2;
private int emptyViewLayoutId;
private boolean isEmpty;
public ParseAdapter(Context c, QueryFactory<? extends ParseObject> q) {
super(c, q);
addOnQueryLoadListener(new OnQueryLoadListener() {
#Override
public void onLoading() {
isEmpty = false;
}
#Override
public void onLoaded(List list, Exception e) {
if (list == null || list.size() == 0) {
isEmpty = true;
}
}
});
}
#Override
public void notifyDataSetChanged() {
isEmpty = false;
super.notifyDataSetChanged();
}
public void setEmptyLayoutId(int emptyViewLayoutId) {
this.emptyViewLayoutId = emptyViewLayoutId;
}
#Override
public View getItemView(ParseObject object, View v, ViewGroup parent) {
if (isEmpty) {
v = View.inflate(getContext(), emptyViewLayoutId, null);
return v;
}
v = v != null ? v : View.inflate(getContext(), rowLayoutId, null);
//do whatever you want on v
return v;
}
#Override
public int getViewTypeCount() {
return super.getViewTypeCount() + 1; //3
}
#Override
public int getItemViewType(int position) {
return isEmpty ? EMPTY_VIEW : super.getItemViewType(position);
}
#Override
public int getCount() {
return isEmpty ? 1 : super.getCount();
}
}
You need to add an item view type because otherwise, if empty, getItemView() won't be called in some cases. Overriding getCount() to return 1 is not enough, because ParseQueryAdapter performs some checks over the view type inside getView(), and won't pass the call to getItemView().

Categories

Resources