3

I have the following JSON which I'm parsing in GSON and Retrofit. I want to display the values of the same id in one TextView. What's happening right now is that all the values are added to the array and they are being displayed in separate TextViews. I want to show all values which have the same id to be displayed in one TextView. For eg. id: 240 ab values should be in one TextView. Currently, all ab values are in separate TextView.

This is how the data is currently displaying:

enter image description here

This is how I want the data to be: enter image description here

JSON::

{
  "abc": {
    "1": {
      "1": {
        "ab": "some content",
        "id": "240",
        "key": "value"
      },
      "2": {
        "ab": "some content",
        "id": "240",
        "key": "value"
      },
      "3": {
        "ab": "some content",
        "id": "240",
        "key": "value"
      }
    },
    "2": {
      "1": {
        "ab": "more content",
        "id": "241",
        "key": "value"
      },
      "2": {
        "ab": "more content 1",
        "id": "241",
        "key": "value"
      },
      "3": {
        "ab": "more content 2",
        "id": "241",
        "key": "value"
      }
    }
  }
}

POJOContent::

public class POJOContent {

    @SerializedName("ab")
    public String content;

    @SerializedName("id")
    public String id;

    @SerializedName("key")
    public String key;

    @Override
    public String toString() {
        return content;
    }

    //getters and setters

}

MyContentWrapper::

public class MyContentWrapper {
    public Map<Integer, MyMap> abc;
}

MyMap::

public class MyMap extends HashMap<Integer, POJOContent> {
    @Override
    public POJOContent put(Integer key, POJOContent value) {
        if(null==value.getContent() || value.getContent().isBlank()) {
            return null;
        }
        // Added only if content = "ab" is not blank.
        return super.put(key, value);
    }
}

Callback:

    Callback<MyContentWrapper> myCallback = new Callback<MyContentWrapper>() {
                @Override
                public void onResponse(Call<MyContentWrapper> call, Response<MyContentWrapper> response) {
                    if (response.isSuccessful()) {
                        Log.d("Callback", " Message: " + response.raw());
                        Log.d("Callback", " Message: " + response.body().abc.values());

                        MyContentWrapper contentWrapper = response.body();

                        List<POJOContent> pojo = new ArrayList<>();

                        for (Map.Entry<Integer, MyMap> entry : contentWrapper.abc.entrySet()) {
                            Integer key = entry.getKey();
                            MyMap map = entry.getValue();
                            if (!map.isEmpty()){
                                Log.d("Callback", " Key: " + key);
                                Log.d("Callback", " Value: " + map.values());

                                pojo.addAll(map.values());
                            }
                        }

                        MyContentViewAdapter adapter = new MyContentViewAdapter(pojo);
                        recyclerView.setAdapter(adapter);
                    } else {
                        Log.d("Callback", "Code: " + response.code() + " Message: " + response.message());
                    }
                }
                @Override
                public void onFailure(Call<MyContentWrapper> call, Throwable t) {
                    t.printStackTrace();
                }
            };

RecyclerAdapter::

                public class MyContentViewAdapter extends RecyclerView.Adapter<MyContentViewAdapter.ViewHolder> {
    private List<POJOContent> data;
    private MyClickListener clickListener;


    public class ViewHolder extends RecyclerView.ViewHolder {
        public TextView text;
        private LinearLayout itemLayout;

        public ViewHolder(View v) {
            super(v);
            text = (TextView) v.findViewById(R.id.text_content);
        }
    }

    public MyContentViewAdapter(List<POJOContent> data) {
        this.data = data;
        Log.d("Recyclerview Data", data.toString());
    }


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

    @Override
    public void onBindViewHolder(MyContentViewAdapter.ViewHolder holder, int position) {
        POJOContent pojo = data.get(position);
        Log.d("Recyclerview", pojo.getContent());
        holder.text.setText(pojo.getContent());
        holder.itemView.setTag(pojo.getContent());
    }

    public void setOnItemClickListener(MyClickListener clickListener) {
        this.clickListener = clickListener;
    }

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

}
input
  • 7,503
  • 25
  • 93
  • 150
  • 1
    could you share screenshots of what you have right now and what you are trying to achieve? I don't get what your problem is... Since you only have one TextView in ViewHolder, what do you mean by saying they are being displayed in separate TexViews? Maybe you want values with the same id to be displayed in single ViewHolder? – Rinat Diushenov Jul 26 '20 at 07:45
  • @Rinat, yes that's what I want. – input Jul 26 '20 at 15:41
  • in your json after "abc" key ,under "1":key there are 3 elements with id 240 and under "2" key 3 elements with id 241. My question is is it guaranteed that the all the elements with same id are grouped in the same pattern? – Rinat Diushenov Jul 27 '20 at 04:57
  • and those nested numbers keys like "1" ,"2" ,"3", did you rename them so you wouldn't give away sensitive info or is it real structure of real json response from server? Coz if they had constant names like "products" "subProducts" it would probably be easier to achieve desired result – Rinat Diushenov Jul 27 '20 at 05:30
  • @Rinat, it is guaranteed that all elements with same id are grouped. and those nested number keys are like "1", "2", "3".. i didn't rename them. Unfortunately, I do not have control over the structure of the json. – input Jul 27 '20 at 12:34

6 Answers6

4

EDIT:

I added nested RecyclerView inside ViewHolder, so the content and value fields will be displayed dynamically. I'm Adding full code of 2 Adapter and 2 ViewHolder classes, 2 xml layouts and screenshot

I'm pretty sure it will run really smoothly with very large list too.

Everything under ID(240,241) is another recyclerView.

enter image description here

The idea is that list's size, for adapter to populate itself, should be as many as the number of distinct ids, so that only that many Viewholders are inflated.

 List<List<POJOContent>> listOfPojoLists = new ArrayList<>();

    for (Map.Entry<Integer, MyMap> entry : contentWrapper.abc.entrySet()) {
        Integer key = entry.getKey();
        MyMap map = entry.getValue();
        if (!map.isEmpty()){
            Log.d("Callback", " Key: " + key);
            Log.d("Callback", " Value: " + map.values());

            listOfPojoLists.add(new ArrayList<>(map.values()));
        }
    }

    MyContentViewAdapter adapter = new MyContentViewAdapter(listOfPojoLists);
    recyclerView.setAdapter(adapter);

MyContentViewAdapter.java

public class MyContentViewAdapter extends RecyclerView.Adapter<MyContentViewAdapter.ViewHolder> {
private List<List<POJOContent>> data;
private MyClickListener clickListener;


MyContentViewAdapter(List<List<POJOContent>> data) {
    this.data = data;
}

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

@Override
public void onBindViewHolder(MyContentViewAdapter.ViewHolder holder, int position) {
    holder.bind(data.get(position));
}

public void setOnItemClickListener(MyClickListener clickListener) {
    this.clickListener = clickListener;
}

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


class ViewHolder extends RecyclerView.ViewHolder {

    private TextView textId;
    private InnerListAdapter innerAdapter;

    // inside constructor we are initializing inner recyclerView and inner Adapter.
    // there will only be 3 - 5 instances of them ever created(using this 
    // particular viewHolder layouts), no more.
    // might be slightly more if u use layouts with lower height
    ViewHolder(View v) {
        super(v);
        textId =  v.findViewById(R.id.tv_Id);
        RecyclerView innerRecycler = v.findViewById(R.id.rv_inner_list);
        // I added DividerItemDecoration so it would be clear that there are actually different viewHolders
        // displayed by recyclerView
        innerRecycler.addItemDecoration(new DividerItemDecoration(v.getContext(), DividerItemDecoration.VERTICAL));
        innerAdapter = new InnerListAdapter();
        innerRecycler.setAdapter(innerAdapter);
    }

    /* We just submit new list for our inner adapter
    so it will handle rebinding values to its viewHolders */
    void bind(List<POJOContent> pojoList){
        textId.setText(pojoList.get(0).id);
        innerAdapter.setNewItems(pojoList);
    }
}

}


InnerListAdapter.java

public class InnerListAdapter extends RecyclerView.Adapter<InnerListAdapter.InnerViewHolder> {

private List<POJOContent> items = new ArrayList<>();

@NonNull
@Override
public InnerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new InnerViewHolder(LayoutInflater.from(parent.getContext())
            .inflate(R.layout.item_inner_list, parent, false));
}

@Override
public void onBindViewHolder(@NonNull InnerViewHolder holder, int position) {
    holder.bind(items.get(position));
}

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

void setNewItems(List<POJOContent> newItems){
    items.clear();
    items.addAll(newItems);
    notifyDataSetChanged();
}

class InnerViewHolder extends RecyclerView.ViewHolder{
    TextView tv_value;
    TextView tv_content;

    InnerViewHolder(@NonNull View itemView) {
        super(itemView);
        tv_value = itemView.findViewById(R.id.tv_value);
        tv_content = itemView.findViewById(R.id.tv_content);
    }

    void bind(POJOContent pojoContent){
        tv_value.setText(pojoContent.getKey());
        tv_content.setText(pojoContent.getContent());
    }
}

}


fragment_content_card.xml layout for main recyclerView viewholder

    <?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:padding="8dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_Id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:textSize="32sp"
            android:textColor="@color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="ID" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_inner_list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_Id" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

item_inner_list.xml layout for inner recylerVoews' viewholder

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 
    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:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:textColor="@color/black"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="value" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        android:textColor="@color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_value"
        tools:text="content" />

</androidx.constraintlayout.widget.ConstraintLayout>
Rinat Diushenov
  • 1,215
  • 7
  • 15
  • Thanks for your reply, I really appreciate it. However I don't want to use StringBuilder to append the data in one view as I might want to add other key/values pairs as well. What I'm looking for is to add the values as nested data in the Recyclerview. I've figured one way to do it and that is to create a layout in xml and inflate it in the adapter and add it to this recyclerview layout. – input Jul 28 '20 at 17:11
  • But I've read that this is not the best way to do it and most efficient way would be to use multiple viewholders so each dynamic numeric integer would be "header" and the nested data would be the header's data. Sorry if its too much to ask but can you help me with this? – input Jul 28 '20 at 17:11
  • I really willing to help you, would be awesome if i managed to help someone with such a high score:) but i can't quite understand what you mean :( – Rinat Diushenov Jul 28 '20 at 18:08
  • hmm. So everything up until StringBuilder is fine? Or "List>" is also not preferable?I guess, maybe you want dynamic number of texViews for each "ab"(content) inside single ViewHolder? – Rinat Diushenov Jul 28 '20 at 18:10
  • Maybe we could continue in chat? – Rinat Diushenov Jul 28 '20 at 18:11
  • I have added two images to convey better how I want the output to display. Hope this makes my question clear. – input Jul 29 '20 at 16:55
  • Images helped, thanks! I hope my update is what you wanted to achieve. – Rinat Diushenov Jul 29 '20 at 18:35
  • May you update the full code to help me to better understand – Deepak Shukla Feb 25 '21 at 05:03
2

Based on your question, you have separate issues to tackle:

1 - Define better the data you have. You currently have POJOContent but you want to display something like a POJOContentGroup, so before getting to the view part you pre-process your data and map it from MyContentWrapper to List<POJOContentGroup>.

data class POJOContentGroup(val id: String, val contentList: List<POJOContent>) (This is in Kotlin for brevity)

2 - Having the previous defined, now you have a simple task: Display a list of POJOContentGroup. Create your recycler view based on that, similar to what you've done initially, with the difference that the item layout will be a TextView for the id and a RecyclerView for the contentList, which is the one you currently have.

Douglas Kazumi
  • 1,206
  • 10
  • 14
  • I have a `MyMap` class which extends `HashMap`. When I change it `List`, I get the error of: `Expected BEGIN_ARRAY but was BEGIN_OBJECT`. – input Jul 30 '20 at 15:47
  • You still need `MyMap` as is because it is the representation of the JSON data. My suggestion was to create a new class `POJOContentGroup` that represents the data you want to display and then have a function mapping from `MyContentWrapper` to `List`. – Douglas Kazumi Jul 31 '20 at 08:37
2

create another pojo class for RV List Items with content and keys list

public class POJOListItem {
  public String id;
  public List<String> contents = new ArrayList<>();
  public List<String> keys = new ArrayList<>();
}

then validate and map MyContentWrapper object to List<POJOListItem>, check the list if it contains the id, if it has the same id then add them to the contents and keys list or create a new item and add it. based on the list size dynamically add item in a container layout inside the view holder.

Check out this small demo project https://github.com/toxic-charger/SO-63045236

Tested with 50000+ items no performance issus

Arpan Sarkar
  • 2,301
  • 2
  • 13
  • 24
  • I had actually solved this initially using this method but wasn't sure if inflating the layout on `BindViewHolder` was performant and the best way to do it. I was wondering if its possible to do it using multiple view types something similar to this: https://stackoverflow.com/questions/26245139/how-to-create-recyclerview-with-multiple-view-type – input Jul 31 '20 at 00:18
  • @input its possible to do it using multiple view types...but in that case you need to create multiple viewHolder with different number of textviews. – Arpan Sarkar Jul 31 '20 at 14:56
  • @input I have update the demo project , so instead of inflating the view every time store the list of inflated views inside View tag and retrieve the view list from the view tag and reuse https://github.com/toxic-charger/SO-63045236/commit/8b4a164af069ee146aa9a574833529b12a1162c0 – Arpan Sarkar Jul 31 '20 at 14:59
1

As per my understanding of problem description, you are trying to construct array like this

[
"some content"+"some content"+"some content", // id = 240
"more content"+"more content 1"+"more content 2" // id = 241
]

and you want to show these 2 values in list of TextView

but the array you constructed will be,

[
"some content",
"some content",
"some content",
"more content",
"more content 1",
"more content 2"
]

and hence each entry is displayed in separate TextView

To debug further, please check value of pojo list after

pojo.addAll(map.values())
// may be if you want to group entries with comma delimiter, use
// pojo.addAll(TextUtils.join(", ", map.values()))
Jegan Babu
  • 1,286
  • 1
  • 15
  • 19
  • Yes, that's exactly how the array is constructed, but I want the array to be constructed like how you demonstrated in the first array and show it in a single viewholder. – input Jul 26 '20 at 15:42
  • You need to combine map.values() into single value before adding to pojo list. I have mentioned in the last line "TextUtils.join(", ", map.values())" to merge values with comma. Please check if it works for your use case. – Jegan Babu Jul 26 '20 at 18:04
  • thanks for your reply, but is there a way to do it by inflating the textview and adding the nested data? – input Jul 27 '20 at 12:49
  • I have added two images to convey better how I want the output to display. Hope this makes my question clear. – input Jul 29 '20 at 16:58
1

Hi its late but i would say create one class ListItem

class ListItem {
    private var item: HashMap<Int, POJOContent>? = null
}

and overwrite toString in your POJOContent to return the values you want to display on text view like

@Override
public String toString() {
    return value + "\n" + content;
}

Then use List in your adapter Then in bind

//take care of null checks and you can use System.getProperty("line.separator"); in place of line saperator also don't forgate to set maxLine property of your text view to eome higher value
void bind(ListItem item){
    Array<POJOContent> contents = item.values().toArray()
    tv_value.setText(contents[0].getKey());
    tv_content.setText(contents.toString().replace("[","").replace("]","").replace(",","\n"));
}
alokHarman
  • 289
  • 1
  • 8
0

I think nested-expandable-recyclerview should be the right choice to achieve your goal, which will also increase your user experience. By default can open the nested recycler with your child data of the JSON.

enter image description here

Gk Mohammad Emon
  • 6,084
  • 3
  • 42
  • 42