Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewPager fragments are all referencing the same RecyclerView and/or Adapter

I have a Fragment that contains a RecyclerView to display events for a given day. I am using a ViewPager to separate the Fragments into multiple days; A Fragment for Saturday's events and a Fragment for Sunday's events.

However, it appears that both Fragments are referencing the same RecyclerView and/or Adapter, as it is only the last tab (in this case, Sunday) whose events are shown.

In my specific case, Saturday has two events, and Sunday has no events. Both Fragments have empty RecyclerViews. To confirm my theory that it was caused by the last tab, I switched the date. This caused both RecyclerViews to have two events (the ones from Saturday).

Here is the relevant code for the individual Fragments:

public class EventListFragment extends Fragment{
    private EventAdapter mEventAdapter;

    private static final String DATE_ARG = "eventDate";

    public static EventListFragment newInstance(LocalDate date){
        EventListFragment eventListFragment = new EventListFragment();
        Bundle args = new Bundle();
        args.putSerializable(DATE_ARG, date);
        eventListFragment.setArguments(args);
        return eventListFragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_event_list, container, false);

        // Setup recyclerview
        RecyclerView eventRecyclerView = (RecyclerView) view.findViewById(R.id.event_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        eventRecyclerView.setLayoutManager(layoutManager);

        // Get date
        LocalDate eventDate = (LocalDate) getArguments().getSerializable(DATE_ARG);

        // Set adapter
        mEventAdapter = new EventAdapter(getActivity(), getEvents(eventDate));
        eventRecyclerView.setAdapter(mEventAdapter);

        return view;
    }
}

getEvents() is just a private function to return events for a given date. I have used the debugger as well as unit tests to verify that it works properly. The debugger shows that it pulls the proper list for each Fragment, but as I explained they are not displayed properly.

Here is the relevant code for the parent Fragment:

public class EventFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_event, container, false);

        // Get and set up viewpager
        final ViewPager viewPager = (ViewPager) view.findViewById(R.id.event_view_pager);
        EventFragmentAdapter eventFragmentAdapter = new EventFragmentAdapter(getFragmentManager(), getEventDates());
        viewPager.setAdapter(eventFragmentAdapter);

        // Get and set up tablayout
        final TabLayout tabLayout = (TabLayout) view.findViewById(R.id.event_tabs);
        tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

        tabLayout.post(new Runnable() {
            @Override
            public void run() {
                tabLayout.setupWithViewPager(viewPager);
            }
        });

        return view;
    }
}

Similar to the last one, getEventDates() just pulls the dates that events are taking place. For testing purposes at the moment, I am hard coding a returned list of dates as we don't have our database set up yet. I pulled this method out because I want the app to be able to function again in 2016, which may have different dates:

private List<LocalDate> getEventDates(){
    List<LocalDate> eventDates = new ArrayList<>();

    eventDates.add(new LocalDate(2015, 10, 17));
    eventDates.add(new LocalDate(2015, 10, 18));

    return eventDates;
}

The last bit of relevant code is for the FragmentStatePagerAdapter I am using for my ViewPager:

public class EventFragmentAdapter extends FragmentStatePagerAdapter {
    private List<LocalDate> mEventDates;

    public EventFragmentAdapter(FragmentManager fragmentManager, List<LocalDate> eventDates){
        super(fragmentManager);
        this.mEventDates = eventDates;
    }
    @Override
    public Fragment getItem(int i) {
        return EventListFragment.newInstance(mEventDates.get(i));
    }

    @Override
    public int getCount() {
        return mEventDates.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mEventDates.get(position).dayOfWeek().getAsText();
    }
}

Any ideas why both lists are always the same, and are based on the last tab in the ViewPager? I assume that somehow they are referencing the same RecyclerView or the same RecyclerViewAdapter, but I don't have any static fields for those so I am not sure how it is happening.

like image 363
AdamMc331 Avatar asked Aug 02 '15 16:08

AdamMc331


People also ask

Does ViewPager use RecyclerView?

If you want ViewPager 's behaviour (one item visible at a time, swipe limited to one item and snapping to show the full item) then go with a ViewPager . It's possible but not trivial to replicate this behaviour using a RecycleView .

What is ViewPager adapter?

ViewPager in Android is a class that allows the user to flip left and right through pages of data. This class provides the functionality to flip pages in app. It is a widget found in the support library. To use it you'll have to put the element inside your XML layout file that'll contain multiple child views.

What is the relationship between RecyclerView adapter and RecyclerView ViewHolder?

ViewHolder is not bound to an item or the given RecyclerView. Adapter is not part of this Adapter (if this Adapter merges other adapters).

What is difference between ViewPager and ViewPager2?

ViewPager2 is an improved version of the ViewPager library that offers enhanced functionality and addresses common difficulties with using ViewPager . If your app already uses ViewPager , read this page to learn more about migrating to ViewPager2 .


2 Answers

A long hunt and an anti-climactic solution(as with most difficult bugs). Also a bit unfair since the bug isn't in the code posted above, I had to hunt down your git project to figure it out. The bug is in EventAdapter:

public class EventAdapter extends RecyclerView.Adapter<EventAdapter.ViewHolder> {
    private static final List<Event> mEvents;
    private final Context mContext;

    public EventAdapter(Context context, List<Event> events){
        this.mContext = context;
        mEvents = events;
    }
    ...
}

mEvents is static!... so it's shared across all instances of mEvents. This explains the bug perfectly since updates to it last will set the values for all EventAdapters.

It looks like you made mEvents static so that you could access it within your ViewHolders. Instead you can just store the individual Event within the ViewHolder and drop the dangerous static modifier. On the flip-side, hooray for Open Source Projects!

like image 68
Trevor Carothers Avatar answered Oct 22 '22 04:10

Trevor Carothers


I've seen your code and I totally agree with Travor - you're using a static member, that is replaced every time you create a new Adapter (and so it gets just the last page data). I've modified your project a little bit, to make it work properly. Take a look at it, hope it can be useful.

EventFragment: replace getFragmentManager with getChildFragmentManager since you need EventListFragment to be handle by EventFragment fragment manager and not by the activity fragment manager

public class EventFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_event, container, false);

        // Get and set up viewpager
        final ViewPager viewPager = (ViewPager) view.findViewById(R.id.event_view_pager);
        EventFragmentAdapter eventFragmentAdapter = new EventFragmentAdapter(getChildFragmentManager(), getEventDates());
        viewPager.setAdapter(eventFragmentAdapter);

        // Get and set up tablayout
        final TabLayout tabLayout = (TabLayout) view.findViewById(R.id.event_tabs);
        tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

        tabLayout.post(new Runnable() {
            @Override
            public void run() {
                tabLayout.setupWithViewPager(viewPager);
            }
        });

        return view;
    }

    /**
     * Retrieves the event dates for the hackathon so that the proper events can be displayed.
     * @return
     */
    private List<LocalDate> getEventDates(){
        List<LocalDate> eventDates = new ArrayList<>();

        eventDates.add(new LocalDate(2015, 10, 17));
        eventDates.add(new LocalDate(2015, 10, 18));

        return eventDates;
    }
}

EventListFragment - I've modified the query, since the sqlite query doesn't work with my locale (italian)

public class EventListFragment extends Fragment{

    private EventAdapter mEventAdapter;

    private static final String TAG = EventListFragment.class.getSimpleName();

    private static final String DATE_ARG = "eventDate";

    public static EventListFragment newInstance(LocalDate date){
        EventListFragment eventListFragment = new EventListFragment();
        Bundle args = new Bundle();
        args.putSerializable(DATE_ARG, date);
        eventListFragment.setArguments(args);
        return eventListFragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_event_list, container, false);

        // Setup recyclerview
        RecyclerView eventRecyclerView = (RecyclerView) view.findViewById(R.id.event_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        eventRecyclerView.setLayoutManager(layoutManager);

        // Get date
        LocalDate eventDate = (LocalDate) getArguments().getSerializable(DATE_ARG);

        // Set adapter
        mEventAdapter = new EventAdapter(getEvents(eventDate));
        eventRecyclerView.setAdapter(mEventAdapter);

        Log.v(TAG, eventRecyclerView.toString());

        return view;
    }

    /**
     * Retrieves the events for the given date for the fragment.
     */
    private List<Event> getEvents(LocalDate date){
        List<Event> returnList = new ArrayList<>();

        String dateString = Utility.getDBDateString(date);
        List<String> dateList = new ArrayList<>();

        Cursor cursor = getActivity().getContentResolver().query(
                GHContract.EventEntry.CONTENT_URI,
                new String[]{ "*", "substr(" + GHContract.EventEntry.COLUMN_TIME + ",0,11)" },
                "substr(" + GHContract.EventEntry.COLUMN_TIME + ",0,11) = ? ",
                new String[]{dateString},
                GHContract.EventEntry.COLUMN_TIME
        );

        while(cursor.moveToNext()){
            returnList.add(new Event(cursor));
            dateList.add(cursor.getString(cursor.getColumnIndex(GHContract.EventEntry.COLUMN_TIME)));
        }

        cursor.close();

        return returnList;
    }
}

EventAdapter - removed the static keyword and the reference to the activity context (you need to get the context somewhere else)

public class EventAdapter extends RecyclerView.Adapter<EventAdapter.ViewHolder> {
    private List<Event> mEvents;

    public EventAdapter(List<Event> events){
        mEvents = events;
    }

    /**
     * Inflates the view for Event items.
     */
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_event, parent, false);
        return new ViewHolder(view);
    }

    /**
     * Binds the data for an event to its view.
     */
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Event event = mEvents.get(position);
        holder.timeView.setText(Utility.getTimeString(event.getTime()));
        holder.titleView.setText(event.getTitle());
        holder.locationView.setText(event.getLocation());

        // If reminder time is not null, show check mark. If it is, show plus.
        if(event.getReminderTime() != null){
            holder.alarmView.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.ic_alarm_on));
        } else{
            holder.alarmView.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.ic_add_alarm));
        }
    }

    /**
     * Returns the size of the adapter.
     */
    @Override
    public int getItemCount() {
        return mEvents.size();
    }

    /**
     * Retains a reference to the view so `findViewById` calls are only made once for the adapter.
     */
    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
        public final TextView timeView;
        public final TextView titleView;
        public final TextView locationView;
        public final ImageView alarmView;

        public ViewHolder(View view){
            super(view);
            timeView = (TextView) view.findViewById(R.id.event_time);
            titleView = (TextView) view.findViewById(R.id.event_title);
            locationView = (TextView) view.findViewById(R.id.event_location);
            alarmView = (ImageView) view.findViewById(R.id.event_add_reminder);
            alarmView.setOnClickListener(this);
        }

        /**
         * Handles the click a user makes on the alarm image view.
         */
        @Override
        public void onClick(View v) {
            OnEventReminderClickListener activity = (OnEventReminderClickListener) v.getContext();
            activity.onEventReminderClicked(mEvents.get(getPosition()));
        }
    }

    /**
     * Interface to call back to the activity when an alarm is clicked for an event item.
     */
    public interface OnEventReminderClickListener{
        void onEventReminderClicked(Event event);
    }
}

And finally the app/build.gradle, since you need to get the same version for all support libraries (recycler, card and so on)

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.adammcneilly.grizzhacks"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'joda-time:joda-time:2.7'
    compile 'com.android.support:recyclerview-v7:22.2.1'
    compile 'com.android.support:cardview-v7:22.2.1'
    compile 'com.android.support:design:22.2.1'
}
like image 22
Mimmo Grottoli Avatar answered Oct 22 '22 04:10

Mimmo Grottoli