Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android getListView() in fragment error

I keep having an issue with my android app where it is crashing with the following error when swiping between tabs:

09-16 16:19:27.142    4750-4750/com.khackett.runmate E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.khackett.runmate, PID: 4750
    java.lang.IllegalStateException: Content view not yet created
            at android.support.v4.app.ListFragment.ensureList(ListFragment.java:328)
            at android.support.v4.app.ListFragment.getListView(ListFragment.java:222)
            at com.khackett.runmate.ui.MyRunsFragment$1.done(MyRunsFragment.java:167)
            at com.khackett.runmate.ui.MyRunsFragment$1.done(MyRunsFragment.java:135)
            at com.parse.ParseTaskUtils$2$1.run(ParseTaskUtils.java:115)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5254)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

This is the MyRunsFragment:

public class MyRunsFragment extends ListFragment {

    protected SwipeRefreshLayout mSwipeRefreshLayout;

    // member variable to store the list of routes the user has accepted
    protected List<ParseObject> mAcceptedRoutes;

    private int MY_STATUS_CODE = 1111;

    // Default constructor for MyRunsFragment
    public MyRunsFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View rootView = inflater.inflate(R.layout.fragment_my_runs, container, false);

        // Set SwipeRefreshLayout component
        mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipeRefreshLayout);
        // Set the onRefreshListener
        mSwipeRefreshLayout.setOnRefreshListener(mOnRefreshListener);
        mSwipeRefreshLayout.setColorSchemeResources(
                R.color.swipeRefresh1,
                R.color.swipeRefresh2,
                R.color.swipeRefresh3,
                R.color.swipeRefresh4);

        return rootView;
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        // Retrieve the accepted routes from the Parse backend
        retrieveAcceptedRoutes();
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);

        // create the message object which is set to the message at the current position
        ParseObject route = mAcceptedRoutes.get(position);

        // String messageType = message.getString(ParseConstants.KEY_FILE_TYPE);

        JSONArray parseList = route.getJSONArray(ParseConstants.KEY_LATLNG_POINTS);
        JSONArray parseListBounds = route.getJSONArray(ParseConstants.KEY_LATLNG_BOUNDARY_POINTS);
        String objectId = route.getObjectId();
        String routeName = route.getString(ParseConstants.KEY_ROUTE_NAME);
        // JSONArray ids = route.getJSONArray(ParseConstants.KEY_RECIPIENT_IDS);

        // Start a map activity to display the route
        Intent intent = new Intent(getActivity(), MapsActivityTrackRun.class);
        intent.putExtra("parseLatLngList", parseList.toString());
        intent.putExtra("parseLatLngBoundsList", parseListBounds.toString());
        intent.putExtra("myRunsObjectId", objectId);
        intent.putExtra("myRunsRouteName", routeName);

        // Start the MapsActivityDisplayRoute activity
        startActivityForResult(intent, MY_STATUS_CODE);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {

    }

    private void retrieveAcceptedRoutes() {
        // query the routes class/table in parse
        // get messages where the logged in user ID is in the list of the recipient ID's (we only want to retrieve the messages sent to us)
        // querying the message class is similar to how we have been querying users
        ParseQuery<ParseObject> queryRoute = new ParseQuery<ParseObject>(ParseConstants.CLASS_ROUTES);
        // use the 'where' clause to search through the messages to find where our user ID is one of the recipients
        queryRoute.whereEqualTo(ParseConstants.KEY_ACCEPTED_RECIPIENT_IDS, ParseUser.getCurrentUser().getObjectId());
        // order results so that most recent message are at the top of the inbox
        queryRoute.addDescendingOrder(ParseConstants.KEY_CREATED_AT);
        // query is ready - run it
        queryRoute.findInBackground(new FindCallback<ParseObject>() {
            // When the retrieval is done from the Parse query, the done() callback method is called
            @Override
            public void done(List<ParseObject> routes, ParseException e) {
                // dismiss the progress indicator here
                // getActivity().setProgressBarIndeterminateVisibility(false);

                // End refreshing once routes are retrieved
                // done() is called from onResume() and the OnRefreshListener
                // Need to check that its called from the the OnRefreshListener before ending it
                if (mSwipeRefreshLayout.isRefreshing()) {
                    mSwipeRefreshLayout.setRefreshing(false);
                }

                // the list being returned is a list of routes
                if (e == null) {
                    // successful - routes found.  They are stored as a list in messages
                    mAcceptedRoutes = routes;

                    // adapt this data for the list view, showing the senders name

                    // create an array of strings to store the usernames and set the size equal to that of the list returned
                    String[] usernames = new String[mAcceptedRoutes.size()];
                    // enhanced for loop to go through the list of users and create an array of usernames
                    int i = 0;
                    for (ParseObject message : mAcceptedRoutes) {
                        // get the specific key
                        usernames[i] = message.getString(ParseConstants.KEY_SENDER_NAME);
                        i++;
                    }

                    // Create the adapter once and update its state on each refresh
                    if (getListView().getAdapter() == null) {
                        // the above adapter code is now replaced with the following line
                        RouteMessageAdapter adapter = new RouteMessageAdapter(getListView().getContext(), mAcceptedRoutes);

                        // Force a refresh of the list once data has changed
                        adapter.notifyDataSetChanged();

                        // need to call setListAdapter for this activity.  This method is specifically from the ListActivity class
                        setListAdapter(adapter);
                    } else {
                        // refill the adapter
                        // cast it to RouteMessageAdapter
                        ((RouteMessageAdapter) getListView().getAdapter()).refill(mAcceptedRoutes);
                    }
                }
            }
        });
    }

    protected SwipeRefreshLayout.OnRefreshListener mOnRefreshListener = new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            // When list is swiped down to refresh, retrieve the users runs from the Parse backend
            retrieveAcceptedRoutes();
        }
    };

}

And the fragment_my_runs layout file:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity$PlaceholderFragment">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true">

        <ListView
            android:id="@android:id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"
            android:clipToPadding="false"
            android:paddingBottom="@dimen/inbox_vertical_margin"/>

    </android.support.v4.widget.SwipeRefreshLayout>

    <TextView
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/empty_inbox_label"
        android:textSize="@dimen/default_text_size"/>

</RelativeLayout>

The TabFragmentContainer

public class TabFragmentContainer extends Fragment {

    // Create the FragmentPagerAdapter that will provide and manage tabs for each section.
    public static MyFragmentPagerAdapter myFragmentPagerAdapter;

    public static TabLayout tabLayout;

    // The ViewPager is a layout widget in which each child view is a separate tab in the layout.
    // It will host the section contents.
    public static ViewPager viewPager;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        // Inflate tab_layout_fragment_container view and setup views for the TabLayout and ViewPager items.
        View view = inflater.inflate(R.layout.tab_layout_fragment_container, null);

        tabLayout = (TabLayout) view.findViewById(R.id.tabs);

        // Set up the ViewPager with the sections adapter.
        viewPager = (ViewPager) view.findViewById(R.id.viewpager);

        // Instantiate the adapter that will return a fragment for each of the three sections of the main activity
        myFragmentPagerAdapter = new MyFragmentPagerAdapter(getActivity(), getChildFragmentManager());

        // Set up the adapter for the ViewPager
        viewPager.setAdapter(myFragmentPagerAdapter);

        // Runnable() method required to implement setupWithViewPager() method
        tabLayout.post(new Runnable() {
            @Override
            public void run() {
                tabLayout.setupWithViewPager(viewPager);
                viewPager.setCurrentItem(1, false);
                // tabLayout.getTabAt(1).select();
            }
        });

        // Return the created View
        return view;
    }

}

The FragmentPagerAdapter:

public class MyFragmentPagerAdapter extends FragmentPagerAdapter {

    // The context to be passed in when the adapter is created.
    private Context mContext;
    // The number of tabs in the layout.
    public static int numberOfTabs = 3;

    /**
     * Default constructor that accepts a FragmentManager parameter to add or remove fragments.
     *
     * @param context         the context from the activity using the adapter.
     * @param fragmentManager the FragmentManager for managing Fragments inside of the TabFragmentContainer.
     */
    public MyFragmentPagerAdapter(Context context, FragmentManager fragmentManager) {
        super(fragmentManager);
        mContext = context;
    }

    /**
     * Method to return the relevant fragment for the selected tab.
     */
    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return new MyRunsFragment();
            case 1:
                return new InboxRouteFragment();
            case 2:
                return new FriendsFragment();
        }
        return null;
    }

    /**
     * Method that gets the number of tabs in the layout.
     *
     * @return the number of tabs in the layout.
     */
    @Override
    public int getCount() {
        return numberOfTabs;
    }

    /**
     * Method that returns the title of each tab in the layout.
     */
    @Override
    public CharSequence getPageTitle(int position) {
        Locale locale = Locale.getDefault();
        switch (position) {
            case 0:
                return mContext.getString(R.string.title_section1).toUpperCase(locale);
            case 1:
                return mContext.getString(R.string.title_section2).toUpperCase(locale);
            case 2:
                return mContext.getString(R.string.title_section3).toUpperCase(locale);
        }
        return null;
    }
}

The tab_layout_fragment_container file that contains the ViewPager widget:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/ColorPrimaryPurple"
        app:tabGravity="fill"
        app:tabIndicatorColor="@color/ColorPrimaryPurple"
        app:tabMode="fixed"
        app:tabSelectedTextColor="@color/textColorPrimary"
        app:tabTextColor="@color/pressedPurpleButton">
    </android.support.design.widget.TabLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </android.support.v4.view.ViewPager>

</LinearLayout>

The onCreate() method in my MainActivity:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    // Initialise the DrawerLayout and NavigationView views.
    mDrawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout);
    mNavigationView = (NavigationView) findViewById(R.id.navigationDrawerMenu);

    // Inflate the first fragment to be displayed when logged into the app.
    mFragmentManager = getSupportFragmentManager();
    mFragmentTransaction = mFragmentManager.beginTransaction();
    mFragmentTransaction.replace(R.id.containerView, new TabFragmentContainer()).commit();

    // Setup click events on the NavigationView items.
    // When an item is selected, replace the tab fragment container with the requested fragment.
    mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem menuItem) {
            mDrawerLayout.closeDrawers();
            if (menuItem.getItemId() == R.id.navItemHome) {
                FragmentTransaction tabFragmentContainer = mFragmentManager.beginTransaction();
                tabFragmentContainer.replace(R.id.containerView, new TabFragmentContainer()).commit();
            }
            if (menuItem.getItemId() == R.id.navItemRunHistory) {
                FragmentTransaction runHistoryFragment = mFragmentManager.beginTransaction();
                runHistoryFragment.replace(R.id.containerView, new RunHistoryFragment()).commit();
            }
            if (menuItem.getItemId() == R.id.navItemSettings) {
                FragmentTransaction settingsFragment = mFragmentManager.beginTransaction();
                settingsFragment.replace(R.id.containerView, new SettingsFragment()).commit();
            }
            if (menuItem.getItemId() == R.id.navItemHelp) {
                FragmentTransaction instructionsFragment = mFragmentManager.beginTransaction();
                instructionsFragment.replace(R.id.containerView, new InstructionsFragment()).commit();
            }
            if (menuItem.getItemId() == R.id.navItemMyProfile) {
                FragmentTransaction myProfileFragment = mFragmentManager.beginTransaction();
                myProfileFragment.replace(R.id.containerView, new MyProfileFragment()).commit();
            }
            if (menuItem.getItemId() == R.id.navItemLogOut) {
                // User has selected log out option. Log user out and return to login screen.
                ParseUser.logOut();
                navigateToLogin();
            }
            return false;
        }
    });

    // Set up the Toolbar.
    setupToolbar();
}

I have followed other answers here and added the getListView() functionality to the onViewCreated() method but the problem still persists... Can anyone point out where I might be going wrong?

like image 222
KvnH Avatar asked Sep 16 '15 15:09

KvnH


3 Answers

I read your question again then I guess that:

  • Your ListFragment is destroyed while your background task keeps running. So when it's done, your callback would like to update the ListView which is no longer alive.

  • Actually, viewPager.setOffscreenPageLimit(3); may do the trick, but it's not a good practice. It forces your ViewPager to create and store more Fragments in memory which is not necessary. You can solve this without doing so.

What you should do: one of the following two practice should be fine, or both:

  • Destroy your task in your onPause or whatever lifecycle method, before your onDestroyView.

  • Exclude the code where you update your ListView inside your done() method. Make it a local method where you will check your ListView carefully, and there, you should ask your update process to run on UI thread to avoid any threading problem. Make sure to check if your getView() is not null (but not your getListView(), since it throws Exception if getView() returns null).

I recommend you to use both of them to make sure: your view is still useable and you don't waste your resource when running task in invisible fragment. Don't forget that by default, once your fragment is invisible, it is considered to be destroyed (not always, for example ViewPager keep reference of 2 fragments, but keep in mind that case).

like image 140
Nguyễn Hoài Nam Avatar answered Oct 20 '22 02:10

Nguyễn Hoài Nam


onViewCreated is called immediately after onCreateView, but the super.onViewCreated call is missing, perhaps this is root cause of your issue.

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState); // add this line back in
    // Retrieve the accepted routes from the Parse backend
    retrieveAcceptedRoutes();
}
like image 21
petey Avatar answered Oct 20 '22 03:10

petey


Based on these facts:

  • The exception is thrown because there is no root view yet when done() calls getListView().
  • done() is called when the query made by retrieveAcceptedRoutes() gets a response.
  • retrieveAcceptedRoutes is called in multiple places, including the OnRefreshListener mOnRefreshListener, which is registered as the refresh listener in onCreateView() before there is a root view (that is, before onCreateView() returns).

...it is possible for getListView() to be called before there is a root view.

Try moving these 3 statements from onCreateView() to onViewCreated(), so that way the refresh listener can only be called when there is a root view.

    // Set SwipeRefreshLayout component 
    mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipeRefreshLayout);
    // Set the onRefreshListener 
    mSwipeRefreshLayout.setOnRefreshListener(mOnRefreshListener);
    mSwipeRefreshLayout.setColorSchemeResources(
            R.color.swipeRefresh1,
            R.color.swipeRefresh2,
            R.color.swipeRefresh3,
            R.color.swipeRefresh4);
like image 5
cybersam Avatar answered Oct 20 '22 03:10

cybersam