19

I've four tabs and four fragments (each one for each tab).

Each fragment has a vertical recycler view. Since all fragments view look similar I'm re-using the same layout file, same recycler view items and same adapter.

The issue is that only one item is loaded under the first tab and third tab and fourth tab, While the second tab successfully loads the entire data.

I hope image added below gives better understanding regarding the issue.

enter image description here

Here is my adapter code

public class OthersAdapter extends RecyclerView.Adapter<OthersAdapter.OthersViewHolder> {

    private final Context context;
    private final ArrayList<LocalDealsDataFields> othersDataArray;
    private LayoutInflater layoutInflater;

    public OthersAdapter(Context context, ArrayList<LocalDealsDataFields> othersDataArray) {
        this.context = context;
        this.othersDataArray = othersDataArray;
        if (this.context != null) {
            layoutInflater = LayoutInflater.from(this.context);
        }
    }

    class OthersViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        TextView othersSmallTitleTextView;
        ImageView othersImageView;

        OthersViewHolder(View itemView) {
            super(itemView);
            othersSmallTitleTextView = (TextView) itemView.findViewById(R.id.others_small_title);
            othersImageView = (ImageView) itemView.findViewById(R.id.others_image);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            Intent couponDetailsItem = new Intent(context, LocalDealsActivity.class);
            Bundle extras = new Bundle();
            extras.putString(Constants.SECTION_NAME, context.getString(R.string.local_deals_section_title));
            // Add the offer id to the extras. This will be used to retrieve the coupon details
            // in the next activity
            extras.putInt(Constants.COUPONS_OFFER_ID, othersDataArray.get(
                    getAdapterPosition()).getLocalDealId());
            couponDetailsItem.putExtras(extras);
            context.startActivity(couponDetailsItem);
        }
    }

    @Override
    public OthersViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = layoutInflater.inflate(R.layout.others_items, parent, false);
        return new OthersViewHolder(view);
    }

    @Override
    public void onBindViewHolder(OthersViewHolder holder, int position) {
        String lfImage = othersDataArray.get(position).getLocalDealImage();
        String lfCategoryName = othersDataArray.get(position).getLocalDealSecondTitle();
        if (lfCategoryName != null) {
            // Set the second title
            holder.othersSmallTitleTextView.setText(lfCategoryName);
        }
        if (lfImage != null) {
            if (!lfImage.isEmpty()) {
                // Get the Uri
                Uri lfUriImage = Uri.parse(lfImage);
                // Load the Image
                Picasso.with(context).load(lfUriImage).into(holder.othersImageView);
            }
        }
    }

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

I like to point out couple of things -

  • I've checked other answers on Stack Overflow. They talk about setting the recycler view layout_height to wrap_content. This isn't the issue as the layout_height is already wrap_content and also the second tab loads all the data as expected.

  • And some others answers mentioned to used same versions for all support libraries and I'm already using 25.1.0 version for all the support libraries.

  • Size of the data array is 20 and returning 20 from the adapter's getItemCount() method.

  • The data array has the expected number of items in it and they are not null or empty.

  • Clean build, invalidate/caches doesn't work either.

  • Finally, I'm using FragmentStatePagerAdapter to load the fragments when the tabs are in focus.

EDIT:

This is how I'm parsing the JSON data received

private void parseLocalDeals(String stringResponse) throws JSONException {
    JSONArray localJSONArray = new JSONArray(stringResponse);
    // If the array length is less than 10 then display to the end of the JSON data or else
    // display 10 items.
    int localArrayLength = localJSONArray.length() <= 20 ? localJSONArray.length() : 20;
    for (int i = 0; i < localArrayLength; i++) {
        // Initialize Temporary variables
        int localProductId = 0;
        String localSecondTitle = null;
        String localImageUrlString = null;
        JSONObject localJSONObject = localJSONArray.getJSONObject(i);
        if (localJSONObject.has(JSONKeys.KEY_LOCAL_DEAL_ID)) {
            localProductId = localJSONObject.getInt(JSONKeys.KEY_LOCAL_DEAL_ID);
        }
        if (localJSONObject.has(JSONKeys.KEY_LOCAL_DEAL_CATEGORY)) {
            localSecondTitle = localJSONObject.getString(JSONKeys.KEY_LOCAL_DEAL_CATEGORY);
        }
        if (localJSONObject.has(JSONKeys.KEY_LOCAL_DEAL_IMAGE)) {
            localImageUrlString = localJSONObject.getString(JSONKeys.KEY_LOCAL_DEAL_IMAGE);
        }

        if (localImageUrlString != null) {
            if (!localImageUrlString.isEmpty()) {
                // Remove the dots at the start of the Product Image String
                while (localImageUrlString.charAt(0) == '.') {
                    localImageUrlString = localImageUrlString.replaceFirst(".", "");
                }
                // Replace the spaces in the url with %20 (useful if there is any)
                localImageUrlString = localImageUrlString.replaceAll(" ", "%20");
            }
        }

        LocalDealsDataFields localDealsData = new LocalDealsDataFields();
        localDealsData.setLocalDealId(localProductId);
        localDealsData.setLocalDealSecondTitle(localSecondTitle);
        localDealsData.setLocalDealImage(localImageUrlString);

        localDealsDataArray.add(localDealsData);
    }

    // Initialize the Local Deals List only once and notify the adapter that data set has changed
    // from second time. If you initializeRV the localDealsRVAdapter at an early instance and only
    // use the notifyDataSetChanged method here then the adapter doesn't update the data. This is
    // because the adapter won't update items if the number of previously populated items is zero.
    if (localDealsCount == 0) {
        if (localArrayLength != 0) {
            // Populate the Local Deals list
            // Specify an adapter
            localDealsRVAdapter = new OthersAdapter(context, localDealsDataArray);
            localDealsRecyclerView.setAdapter(localDealsRVAdapter);
        } else {
            // localArrayLength is 0; which means there are no rv elements to show.
            // So, remove the layout
            contentMain.setVisibility(View.GONE);
            // Show no results layout
            showNoResultsIfNoData(localArrayLength);
        }
    } else {
        // Notify the adapter that data set has changed
        localDealsRVAdapter.notifyDataSetChanged();
    }
    // Increase the count since parsing the first set of results are returned
    localDealsCount = localDealsCount + 20;
    // Remove the progress bar and show the content
    prcVisibility.success();
}

parseLocalDeals method is inside a helper class and it is called by using initializeHotels.initializeRV();

initializeRV() initializes the Recycler view, makes a network call to the server and the received data is passed to the parseLocalDeals method. initializeHotels being an instance variable of the Helper class.

EDIT 2:

For those who wants to explore the code in detail, I've moved the part of the code to another project and shared it on Github. Here is the link https://github.com/gSrikar/TabLayout and to understand the hierarchy check out the README file.

Can anyone tell me what I'm missing?

Srikar Reddy
  • 3,650
  • 4
  • 36
  • 58
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/136682/discussion-on-question-by-srikar-reddy-recyclerview-onbindviewholder-called-only). – Bhargav Rao Feb 26 '17 at 16:01

3 Answers3

0

Summary

Solved the layout issue at point 1 replacing a LinearLayout by a RelativeLayout, inverting visibility logic to avoid ghost effect and catching exceptions and preventing them when the related view is not found.

Added point 2 to demonstrate that the visual defect is only present on Marshmallow and Nougat devices.

Finally FragmentStatePagerAdapter loads pages before getting focus so a fix is proposed at point 3 (load all pages and update them when are selected).

Further information in the comments below and @d4h answer.

The fourth page is not using the same layout, only the same RecyclerView and id, perhaps a work in progress. The layout issue can be solved using the same layout that previous pages but I consider this change out of scope.


1. Partially fixed for Marshmallow and Nougat devices. Work in progress.

Update2 Changing LinearLayout by RelativeLayout and inverting visibility logic solves layout issue:

enter image description here

Update: Commenting initializeTrending in all the fragment initializations also works onApi23+

enter image description here

I'll check it later, seems as deals are correctly loaded but then trending is loaded and deals are lost. WIP here.

If trending array empty and trending view gone, deals are not shown, but using invisible are shown

enter image description here

enter image description here


2. You are loading a wrong page on Marshmallow and Nougat devices

FragmentStatePagerAdapter first call to getItem() wrong on Nougat devices

This ended up having nothing to do with the FragmentStatePagerAdapter code. Rather, in my fragment, I grabbed a stored object from an array using the string ("id") that I passed to the fragment in init. If I grabbed that stored object by passing in the position of the object in the array, there was no problem. Only occurs in devices with Android 7.

FragmentStatePagerAdapter - getItem

A FragmentStatePager adapter will load the current page, and one page either side. That is why it logs 0 and 1 at the same time. When you switch to page 2, it will load page 3 and keep page 1 in memory. Then when you get to page 4 it will not load anything, as 4 was loaded when you scrolled to 3 and there is nothing beyond that. So the int that you're being given in getItem() is NOT the page that is currently being viewed, is the one being loaded into memory. Hope that clears things up for you

These comments are confirmed in this branch and commit

All pages load correctly on Lollipop emulator, the last page has an extra issue, see OthersFragment:

enter image description here

enter image description here


3. Initialize all pages at creation and update them on selection.

Increase OffScreenPageLimit so all pages are initialised

Add on page selected/unselected/reselected listener

These changes solve the issue commented below:

/**
 * Implement the tab layout and view pager
 */
private void useSlidingTabViewPager() {
    // Create the adapter that will return a fragment for each of the three
    // primary sections of the activity.
    BottomSectionsPagerAdapter mBottomSectionsPagerAdapter = new BottomSectionsPagerAdapter(getChildFragmentManager());

    // Set up the ViewPager with the sections adapter.
    ViewPager mBottomViewPager = (ViewPager) rootView.findViewById(R.id.local_bottom_pager);
    mBottomViewPager.setOffscreenPageLimit(mBottomSectionsPagerAdapter.getCount());
    mBottomViewPager.setAdapter(mBottomSectionsPagerAdapter);

    TabLayout tabLayout = (TabLayout) rootView.findViewById(R.id.tab_layout);
    tabLayout.setupWithViewPager(mBottomViewPager);
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {

    /**
     * Called when a tab enters the selected state.
     *
     * @param tab The tab that was selected
     */
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        // TODO: update the selected page here
        Log.i(LOG_TAG, "page " + tab.getPosition() + " selected.");
    }

    /**
     * Called when a tab exits the selected state.
     *
     * @param tab The tab that was unselected
     */
    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
        // Do nothing
        Log.i(LOG_TAG, "Page " + tab.getPosition() + " unselected and ");
    }

    /**
     * Called when a tab that is already selected is chosen again by the user. Some applications
     * may use this action to return to the top level of a category.
     *
     * @param tab The tab that was reselected.
     */
    @Override
    public void onTabReselected(TabLayout.Tab tab) {
        // Do nothing
        Log.i(LOG_TAG, "Page " + tab.getPosition() + " reselected.");
    }
});
}

Previous Comments:

Check your LocalFragment getItem() method using breakpoints.

If you select one page, next page is also initialized, and you are sharing the recyclerView, etc.

I would move the initialization outside of getItem() as suggested here:

ViewPager is default to load the next page(Fragment) which you can't change by setOffscreenPageLimit(0). But you can do something to hack. You can implement onPageSelected function in Activity containing the ViewPager. In the next Fragment(which you don't want to load), you write a function let's say showViewContent() where you put in all resource consuming init code and do nothing before onResume() method. Then call showViewContent() function inside onPageSelected. Hope this will help

Read these related questions (the first has possible workarounds to hack the limit to zero):

ViewPager.setOffscreenPageLimit(0) doesn't work as expected

Does ViewPager require a minimum of 1 offscreen pages?

Yes. If I am reading the source code correctly, you should be getting a warning about this in LogCat, something like:

Requested offscreen page limit 0 too small; defaulting to 1

viewPager.setOffscreenPageLimit(couponsPagerAdapter.getCount());

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}
Community
  • 1
  • 1
albodelu
  • 7,931
  • 7
  • 41
  • 84
0

Not much of an answer but too long for a comment.

I have duplicated (almost) your adapter code and it fully works for me. I believe I have done the same as you. I'm using the same layout file, the same item & same adapter for all tabs. I think there are no problems with your adapter code.

I say 'almost' because I had to change a couple of things since I don't have access to your data. I changed your LocalDealsDataField model to include a BitmapDrawable & I changed onBindViewHolder() to handle it.

    BitmapDrawable lfImage = othersDataArray.get(position).getLocalDealImage();
    holder.othersImageView.setBackground(lfImage);

Since there seems to be no problem with your adapter, I would focus on getting the data or setting up the adapter as your problem. Sorry I can't be of help beyond that.

FYI, here's how I setup the adapter in onCreateView()

    rootView = inflater.inflate(R.layout.recycler_view, container, false);
    mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
    mAdapter = new OthersAdapter(this.getContext(), list);
    mRecyclerView.setAdapter(mAdapter);
Gary99
  • 1,750
  • 1
  • 19
  • 33
  • I've added the code to Github and you'll find the link in the question. Also, Here is the sample JSON data https://github.com/gSrikar/TabLayout/tree/master/samplejson – Srikar Reddy Feb 22 '17 at 05:07
0

I have looked at your code, problem is same as explained by @ardock

Solution i would like to propose,

You have to change your code at 3 place ::

  1. Inside all Fragment You are using in ViewPager Don't call initializeRESPECTIVEView() from onCreateView method.

  2. Inside LocalFragment make a list of Fragments you are going to use with ViewPager and pass it to BottomSectionsPagerAdapter. and return Fragment from that list from getItem(int position) of BottomSectionsPagerAdapter.

  3. Add Following code to LocalFragment inside useSlidingTabViewPager().

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {   

    @Override
    public void onTabSelected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {

    }
});

//Call Respective fragment initializeRESPECTIVEView() method from onTabSelected , you can get fragment instance from list you passed to BottomSectionsPagerAdapter

Randheer
  • 984
  • 6
  • 24