3

I'm working on a simple Android application (to-do list), and I'm stuck on attempting to implement a ViewPager using a FragmentStatePagerAdapter to navigate through days with tasks. For simplicity I added some tasks for current day, day before, and day after today (not dealing with days without tasks yet, just to keep it simple).

I'm not able to achieve loading proper days when screen is scrolled. When I first swipe left to day before, I get correct result - day before is shown. But then I'm not able to swipe left to get more days (I thought that my code will set the previous "left" date as current "middle" date and let me swipe left again). After trying to scroll to the right-most date and then to left again, I end up with NullPointerException, but I can't figure out how should I change my code to refresh dates properly. Stack trace after sequence of swipes (left-right-right-left):

04-05 22:38:24.966 12441-12441/ptmprojects.com.quicktickcalendar2 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: ptmprojects.com.quicktickcalendar2, PID: 12441
    java.lang.NullPointerException: Attempt to invoke virtual method 'android.support.v4.app.FragmentTransaction android.support.v4.app.FragmentTransaction.add(int, android.support.v4.app.Fragment)' on a null object reference
        at android.support.v4.app.FragmentStatePagerAdapter.instantiateItem(FragmentStatePagerAdapter.java:123)
        at android.support.v4.view.ViewPager.addNewItem(ViewPager.java:1004)
        at android.support.v4.view.ViewPager.populate(ViewPager.java:1186)
        at android.support.v4.view.ViewPager.populate(ViewPager.java:1086)
        at android.support.v4.view.ViewPager$3.run(ViewPager.java:267)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:933)
        at android.view.Choreographer.doCallbacks(Choreographer.java:742)
        at android.view.Choreographer.doFrame(Choreographer.java:671)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:919)
        at android.os.Handler.handleCallback(Handler.java:761)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:156)
        at android.app.ActivityThread.main(ActivityThread.java:6605)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:999)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:889)

I created a TasksBank, where all tasks are stored in List<SingleTask> mAllTasksList, to get tasks from that bank to load them into ViewPager when the proper day shows up on the screen. Tasks are loaded properly for each day as I can see in other Activity started when clicking on Button from ActionBar. Here is a screenshot to confirm that tasks are loaded properly from TasksBank for different days:

All tasks loaded properly

Tasks are represented by class SimpleTask and there is a field mDate in each SimpleTask to store day assigned to each task (I know that Map<LocalDate, SimpleTask> could be a better choice, but for simplicity I have chosen to start with ArrayList<SimpleTask> - if it was a really bad idea, please correct me).

To make you better understand how does it look like, here is a screenshot of how SingleDayFragment looks like (it's a list of SingleTasks stored in RecyclerView):

SingleFragment storing tasks for one day

My thought of solving it was to create a List<LocalDate> mListOfDates in DailyTaskPagerActivity that would store 3 dates - date for task that would show up on left-swipe, date for middle screen and date for the one showing on right-swipe. After scrolling to another day, I should some way set the previous left or right fragment as a current middle one and add another day to list of days when needed. I also created fields: currentPosition and currentDate to store details of currently displayed screen.

I include .java files that I think are relevant to problem that I'm facing, but I think the first one - DailyTaskPagerActivity.java is the one that I have a problem with. If it's too much or not enough in that case, please let me know - I kindly include everything what is needed with proper edit or delete what shouldn't be there.


DailyTaskPagerActivity.java (main class responsible for scrolling between days - I'm pretty sure here's a problem). I made some explanations of what is a purpose of including each element of code in comments:

public class DailyTaskPagerActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private List<LocalDate> mListsOfDates;
    private TasksDaysStatePagerAdapter mAdapter;
    int currentPosition;
    LocalDate currentDate;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_daily_task_pager);

        mViewPager = (ViewPager) findViewById(R.id.daily_task_view_pager);
        mListsOfDates = new ArrayList<>();
        mListsOfDates.add(new LocalDate().minusDays(1));
        mListsOfDates.add(new LocalDate());
        mListsOfDates.add(new LocalDate().plusDays(1));
        currentPosition = 1; // current position on middle screen / today's date
        currentDate = new LocalDate();

        mAdapter = new TasksDaysStatePagerAdapter(getSupportFragmentManager());
        mViewPager.setAdapter(mAdapter);
        mViewPager.setCurrentItem(1); // to start mViewPager from the middle date (to allow swipe to the left and
                                      // to the right (not only to right if it were 0
    }

    private class TasksDaysStatePagerAdapter extends FragmentStatePagerAdapter {

        public TasksDaysStatePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            if (position > currentPosition) { // if position after swipe is higher than currentPosition(equal to 1)
                // it means that the screen has been swiped to the right
                currentPosition = position; // update current position field
                currentDate = currentDate.plusDays(1); // update currentDate field
                mListsOfDates.set(2, currentDate); // set updated date to mListsOfDates last element
                notifyDataSetChanged(); // notify that change to mListsOfDates occured
                return SingleDayFragment.newInstance(currentDate); // create new Fragment with updated date
            } else if (position < currentPosition) {// if position after swipe is lower than currentPosition(equal to 1)
                // it means that the screen has been swiped to the left
                currentPosition = position;
                currentDate = currentDate.minusDays(1);
                mListsOfDates.set(0, currentDate);
                notifyDataSetChanged();
                return SingleDayFragment.newInstance(currentDate);
            } else { // I'm not sure if I should include anything here ??
                // currentPosition = position;
                // mViewPager.setAdapter(new TasksDaysStatePagerAdapter(getSupportFragmentManager()));
                return SingleDayFragment.newInstance(currentDate);
            }
        }

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

SingleDayFragment.java (Fragment for one day, containing TextView with date and RecyclerView with tasks for single day)

public class SingleDayFragment extends Fragment {
    private static final String ARG_DATE = "ptmprojects.com.quicktickcalendar.argDate";
    private RecyclerView mTaskRecyclerView;
    private TaskAdapter mAdapter;
    private TextView mDateAboveRecyclerView;
    DateTimeFormatter mDateTimeFormatter = DateTimeFormat.forPattern("dd MMMM yyyy");
    private LocalDate date;

    public static SingleDayFragment newInstance(LocalDate date) {
        Bundle args = new Bundle();
        args.putSerializable(ARG_DATE, date);
        SingleDayFragment fragment = new SingleDayFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
        date = getArguments().getSerializable(ARG_DATE) != null ? (LocalDate) getArguments().getSerializable(ARG_DATE) : new LocalDate();
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_single_day, container, false);
        mTaskRecyclerView = (RecyclerView) view.findViewById(R.id.single_day_recycler_view);
        mTaskRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mDateAboveRecyclerView = view.findViewById(R.id.date_above_recycler_view);
        mDateAboveRecyclerView.setText(getString(R.string.date_above_recycler_view, date.toString(mDateTimeFormatter)));

        updateUI();
        return view;
    }

    private void updateUI() { // updating RecyclerView containing tasks for one day
        TasksBank tasksBank = TasksBank.get(getActivity());
        List<SingleTask> tasksForSingleDay = tasksBank.getTasksForDate(date);

        if (mAdapter == null) {
            mAdapter = new TaskAdapter(tasksForSingleDay);
            mTaskRecyclerView.setAdapter(mAdapter);
        } else {
            mAdapter.setTasks(tasksForSingleDay);
            mAdapter.notifyDataSetChanged();
        }

    }

    // private class TaskAdapter...
    // private class TaskHolder...
}

AndroidManifest.xml (deleted some lines that I think are not needed):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ptmprojects.com.quicktickcalendar">

    <application
        android:allowBackup="true"
        <activity android:name=".DailyTaskPagerActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".SingleDayActivity">
        </activity>
    </application>

</manifest>

TasksBank.java (class to provide lists of task for each day):

public class TasksBank {

    private Context mContext;
    private static TasksBank sTasksBank;
    private List<SingleTask> mAllTasksList;

    public static TasksBank get(Context context) {
        if (sTasksBank == null) {
            sTasksBank = new TasksBank(context);
        }
        return sTasksBank;
    }

    private TasksBank(Context context) {
        mAllTasksList = new ArrayList<>();
        // Just a sample, to check if code is working
        mContext = context.getApplicationContext();
        mAllTasksList.add(new SingleTask(new LocalDate().minusDays(2), "Title2DaysBefore", "Description2DaysBefore", false));
        mAllTasksList.add(new SingleTask(new LocalDate().minusDays(1), "Title1DayBefore", "Description1DayBefore", false));
        mAllTasksList.add(new SingleTask(new LocalDate(), "Title2", "Description2", false));
        mAllTasksList.add(new SingleTask(new LocalDate().plusDays(1), "Title1DayAfter", "Description1DayAfter", false));
        mAllTasksList.add(new SingleTask(new LocalDate().plusDays(2), "Title2DaysAfter", "Description2DaysAfter", false));
        mAllTasksList.add(new SingleTask(new LocalDate(2018, 03, 31), "Title3", "Description3", false));
    }

    public ArrayList<SingleTask> getTasksForDate(LocalDate date) {
        ArrayList<SingleTask> tasksForDate = new ArrayList<>();
        for(SingleTask task : getAllTasksList()) {
            if (task.getDate().equals(date)) {
                tasksForDate.add(task);
            }
        }
        return tasksForDate;
    }

    public List<SingleTask> getAllTasksList() {
        return mAllTasksList;
    }

    public void addTask(SingleTask task) {
        mAllTasksList.add(task);
    }
}

SingleTask.java (class representing task, there are more fields, getters and setters, but I don't include it as I think it's irrelevant):

public class SingleTask {
    private String mTitle;
    private LocalDate mDate;

    public SingleTask(LocalDate date, String title) {
        mDate = date;
        mTitle = title;
    }

    public LocalDate getDate() {
        return mDate;
    }

    public void setDate(LocalDate date) {
        mDate = date;
    }

    public String getTitle() {
        return mTitle;
    }

    public void setTitle(String title) {
        mTitle = title;
    }
}

Just to let you know that I think that I made a proper amount of research, I include links to some questions that I found here at Stack Overflow, apart from looking outside of Stack Overflow too:

  1. “infinite” scrolling viewpager with days incrementing/decrementing

  2. dynamically add and remove view to viewpager

  3. Update ViewPager dynamically?

  4. Removing fragments from FragmentStatePagerAdapter


In many tutorials I have read that it's trivial to make ViewPager work properly with only implementing getCount() and getItem() methods. How can I get this to work, or can anyone see why it doesn't work now?

Przemysław Moskal
  • 3,551
  • 2
  • 12
  • 21
  • @halfer Thank you for your edit and an explanation. I thought of including both parts that you mention (the one about NPE and the part explaining that I'm stuck on it for a while) just to stress that I haven't asked that question without significant prior research. I've also seen that if only someone mentions in question about NPE, it's immediately flagged as duplicate, so I was just trying to avoid it some way. Of course I accept and appreciate your edit, as it helps to reduce noise in the question. – Przemysław Moskal Apr 05 '18 at 21:24
  • No worries. It's a difficult balance; unfortunately some questions add so much nervous boilerplate that the final question is twice as long as it needs to be, and a jaded readership gets frustrated or loses interest. My suggestion to anyone is "don't say you researched it, just show your research", which I tend to repeat to anyone who believes that "[I have googled](https://stackoverflow.com/search?tab=newest&q=%22I%20have%20googled%22)" is a sufficient talisman against closure `;-p`. – halfer Apr 05 '18 at 21:30
  • (If you really feel commentary about dup voting is necessary, people probably won't object to it in the comments, since it can be trivially deleted there. But, this is just IMHO). – halfer Apr 05 '18 at 21:31
  • @halfer As I'm not the best in reading between lines (English is not my native language), I would like to ask for two clarifications - 1) Is the current shape and look of the question saying that I haven't done enough research? When you say "don't say you reasearch it, show it", did you mean that my question shows that it lacks some research? 2) In your last comment, did you say about... my comment or about the part of the question that you deleted with your edit? :-) If you said about my comment, nothing stops me from deleting it to not attract any votes :-) – Przemysław Moskal Apr 05 '18 at 21:42
  • No, your question is fine, in my view. I was speaking generally about questions on Stack Overflow, and the inventive ways in which people claim to have done research (without actually showing it). This is often done either because they have not done any research at all, or they have done some, but they have not kept an accurate record of what they tried, and they feel that trying to retrace their steps is too much effort. – halfer Apr 05 '18 at 21:46
  • 1
    "inventive ways" - I love how you defined it :-) Ok, now it's clear for me, thank you. – Przemysław Moskal Apr 05 '18 at 21:48
  • 1
    (When I mentioned comments can be deleted: yes, the poster can delete them, but they can also be flagged and deleted by moderators. Mods delete many thousands of "obsolete" comments every day). – halfer Apr 05 '18 at 21:48

0 Answers0