0

I've been having some issues with some fragments of mine and the code they call, which is causing some lag/stutter and was wondering if you might have some advice or insight for me!

I have an app with 4 Fragments and a MainActivity. 2 of my Fragments (called "Locations" and "Food") have LocationRequest code, including getting the user's Location, connecting the GoogleApiClient, etc, in order to obtain the distance between the user's geographical location and the destination of a specific app-defined location.

It would appear, from observing Logcat, that whenever these 2 fragments are more than one swipe away within the ViewPager, and need to be created again, they are calling all the LocationRequest code again. Now this isn't the actual issue I'm having. My issue occurs whenever the fragments call these requests, as they repopulate TextViews within their ListViews with any new information based on the Location data retrieved - and this results in some noticeable lag/stutter in the UI, which I assume is occurring as the TextViews are being updated with any new location data.

So, my question to you is,

  • How would you deal with this issue when moving to a Fragment that needs to be recreated and calls code that causes UI lag?

Can something be done to to make it so that those Fragments that are further than one swipe away are not removed and therefore do not need to be recreated and call all their Location code again?

  • Can I modify the app to hold more Fragments so all 4 of mine are created at app launch and don't need to load in any data that might cause stutter?

  • Should I move my Location-related code to my MainActivity instead of having them held within the Fragments? If that were the case, how would I reference ListViews and Adapters used within my Fragments from the MainActivity (as they are essential to my app)?

I hope this wall of text wasn't too much - hopefully some of you had the patience to stick around and read it all!

Finally, I'll show one of my Fragments and also my MainActivity below.

Locations Fragment:

public class Locations extends Fragment implements GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener, LocationListener {

    public Locations() {
        // Required empty public constructor
    }

    private static final int PERMISSION_REQUEST_ACCESS_FINE_LOCATION = 100;

    private final static int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;

    private static final String LOG_TAG = Locations.class.getName();

    private GoogleApiClient mGoogleApiClient;

    private LocationRequest mLocationRequest;

    Location mLocation = new Location(LocationManager.NETWORK_PROVIDER);

    private ListView mListView;

    public List<Word> locations = new ArrayList<>();

    // If the app already has the permission to access the user's location at high accuracy
    // (fine location), then connect to the GoogleApiClient.
    // If not, no connection-handling methods will be called.
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_ACCESS_FINE_LOCATION: {

                if (ContextCompat.checkSelfPermission(getActivity(),
                        ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {

                    mGoogleApiClient.connect();
                }
            }
        }
    }

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

        mListView = (ListView) rootView.findViewById(R.id.list);

        // The following code checks if the app has permission to access the user's fine location,
        // and requests the permission if necessary:
        if (ContextCompat.checkSelfPermission(getActivity(), ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {

            // Should we show an explanation?
            if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),
                    ACCESS_FINE_LOCATION)) {

                // Show an explanation to the user *asynchronously* -- don't block
                // this thread waiting for the user's response! After the user
                // sees the explanation, try again to request the permission.
                new MaterialStyledDialog.Builder(getActivity())
                        .setTitle(R.string.title_location_permission)
                        .setDescription(R.string.text_location_permission)
                        .setIcon(R.drawable.ic_place_white_48dp)
                        .withDialogAnimation(true)
                        .setPositiveText(android.R.string.ok)
                        .onPositive(new MaterialDialog.SingleButtonCallback() {
                            @Override
                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                                //Prompt the user once explanation has been shown
                                ActivityCompat.requestPermissions(getActivity(),
                                        new String[]{ACCESS_FINE_LOCATION},
                                        PERMISSION_REQUEST_ACCESS_FINE_LOCATION);
                            }
                        })
                        .setNegativeText(android.R.string.no)
                        .show();

            } else {

                // No explanation needed, we can request the permission.
                ActivityCompat.requestPermissions(getActivity(), new String[]{ACCESS_FINE_LOCATION},
                        PERMISSION_REQUEST_ACCESS_FINE_LOCATION);
            }
        }

        if (mGoogleApiClient == null) {
            mGoogleApiClient = new GoogleApiClient.Builder(getActivity())
                    .addConnectionCallbacks(this)
                    .addOnConnectionFailedListener(this)
                    .addApi(LocationServices.API)
                    .build();
        }

        mLocationRequest = LocationRequest.create()
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                .setInterval(10 * 1000)        // 10 seconds, in milliseconds
                .setFastestInterval(1 * 1000); // 1 second, in milliseconds\


        locations.add(new Word("Bristol", R.drawable.bristol,
                41.674009, -71.279006));
        locations.add(new Word("Warren", R.drawable.warren_harbor,
                41.730647, -71.282688));
        locations.add(new Word("Newport", R.drawable.newport_breakers,
                41.486677, -71.315144));
        locations.add(new Word("Jamestown", R.drawable.jamestown,
                41.496313, -71.368435));
        locations.add(new Word("Beavertail Lighthouse", R.drawable.beavertail,
                41.458054, -71.395744));
        locations.add(new Word("Providence", R.drawable.providence,
                41.830279, -71.414955));
        locations.add(new Word("Roger Williams Park", R.drawable.roger_williams_park,
                41.785836, -71.418525));
        locations.add(new Word("Colt State Park", R.drawable.colt_state_park,
                41.682970, -71.297362));
        locations.add(new Word("Blithewold", R.drawable.blithewold,
                41.6540652,-71.2681633));
        locations.add(new Word("Narragansett", R.drawable.narragansett,
                41.433179,-71.457148));
        locations.add(new Word("Barrington", R.drawable.barrington_town_hall,
                41.740674, -71.308610));


        ViewCompat.setNestedScrollingEnabled(mListView, true);

        mListView.setAdapter(new LocationsAdapter(getActivity(), locations, mLocation));

        return rootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onStart() {
        // If the app already has the permission to access the user's location at high accuracy
        // (fine location), then connect to the GoogleApiClient.
        if (ContextCompat.checkSelfPermission(getActivity(),
                ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED || mGoogleApiClient != null) {
            mGoogleApiClient.connect();
        }
        super.onStart();
    }

    @Override
    public void onResume() {
        // If the app already has the permission to access the user's location at high accuracy
        // (fine location), then connect to the GoogleApiClient.
        if (ContextCompat.checkSelfPermission(getActivity(),
                ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            mGoogleApiClient.connect();
        }
        super.onResume();

        // Instantiate the locations ArrayList.
        locations = new ArrayList<>();
    }

    @Override
    public void onPause() {
        super.onPause();
        // If the user has paused the app and the GoogleApiClient is currently connected,
        // remove location updating, and disconnect from the GoogleApiClient.
        if (mGoogleApiClient.isConnected()) {
            LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient,
                    this);
            mGoogleApiClient.disconnect();

            locations.clear();
        }
    }

    public void onStop() {
        // If the user has stopped the app, disconnect from the GoogleApiClient.
        mGoogleApiClient.disconnect();
        super.onStop();
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        Log.i(LOG_TAG, "Location services connected.");

        mLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);

        if (mLocation == null) {
            LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient,
                    mLocationRequest, this);
        } else {
            handleNewLocation(mLocation);
        }
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.i(LOG_TAG, "Location services suspended. Please reconnect.");
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        if (connectionResult.hasResolution()) {
            try {
                // Start an Activity that tries to resolve the error
                connectionResult.startResolutionForResult(getActivity(),
                        CONNECTION_FAILURE_RESOLUTION_REQUEST);
            } catch (IntentSender.SendIntentException e) {
                e.printStackTrace();
            }
        } else {
            Log.i(LOG_TAG, "Location services connection failed with code " +
                    connectionResult.getErrorCode());
        }
    }

    @Override
    public void onLocationChanged(Location location) {

        handleNewLocation(location);
    }

    private void handleNewLocation(Location location) {

        Log.i("TEST handleNewLocation", "Called handleNewLocation");
        // Returns the current adapter in use by the ListView supplied (mListView).
        LocationsAdapter adapter = (LocationsAdapter) mListView.getAdapter();
        adapter.setCurrentLocation(location);
        adapter.notifyDataSetChanged();
    }
}

MainActivity:

public class MainActivity extends AppCompatActivity {

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

        ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);

        CategoryAdapter adapter = new CategoryAdapter(this, getSupportFragmentManager());

        viewPager.setAdapter(adapter);

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

        int[] imageResId = {
                R.drawable.ic_place_white_48dp,
                R.drawable.ic_restaurant_white_48dp,
                R.drawable.ic_wallpaper_white_48dp,
                R.drawable.ic_account_balance_white_48dp};

        // Fragment/Tab Titles
        final String[] mTitleNames = {"Locations", "Food", "Photos", "History"};

        tabLayout.setupWithViewPager(viewPager);

        for (int i = 0; i < imageResId.length; i++) {
            tabLayout.getTabAt(i).setIcon(imageResId[i]);
        }

        // Default actionbar title
        setActionBarTitle(mTitleNames[0]);
        // Set actionbar title when tab is selected
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                int pos = tab.getPosition();
                setActionBarTitle(mTitleNames[pos]);
            }

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

            }

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

            }
        });
    }

    public void setActionBarTitle(String title) {
        getSupportActionBar().setTitle(title);
    }
}

Considering the above, is it possible to move all the Location-request, GoogleApiClient (etc) code from the Fragment and put it into the MainActivity instead?

Wouldn't this mitigate the stutter issue, as the MainActivity is always visible and the code would only be called again when onResume() was called if the app was closed and reopened? If this is possible, and the proper solution (you tell me!), how would I go about moving all the code? I would still need to access the ListViews' adapters from both the Locations fragment, as well as the Food fragment - which are called within private void handleNewLocation(Location location). Specifically, I'd need to be able to modify this code within handleNewLocation so that it still can access the specific ListView and Adapter that each of the 2 Fragments use:

LocationsAdapter adapter = (LocationsAdapter) mListView.getAdapter(); adapter.setCurrentLocation(location); adapter.notifyDataSetChanged();

and

FoodAdapter adapter = (FoodAdapter) mListView.getAdapter(); adapter.setCurrentLocation(location); adapter.notifyDataSetChanged();

Thanks so much for sticking with this long post of mine and thanks for any help!

UPDATE:

So I tried out the method @DanielNugent pointed out and am having some issues (due to my lack of know-how). I tried to follow the information given in the link, but I think my issue is just not knowing how to properly implement the methods... I'm sure I'll get it at some point! Anyway, below...

I moved all the Location-getting code into my MainActivity and tried to make references from the MainActivity back to the Locations and Food fragments - as shown below in the onLocationChanged() method now in the MainActivity:

@Override
    public void onLocationChanged(Location location) {

        FragmentManager fmA = getSupportFragmentManager();
        Locations fragmentA = (Locations)fmA.findFragmentByTag("locationsFragment");
        fragmentA.handleNewLocation(mLocation);

        FragmentManager fmB = getSupportFragmentManager();
        Locations fragmentB = (Locations)fmB.findFragmentByTag("foodFragment");
        fragmentB.handleNewLocation(mLocation);
    }

handleNewLocation() is still residing within the 2 fragments, and I tried having it accessed/called within the MainActivity by the above block of code.

handleNewLocation() is still the same, but I will list it below for reference...

public void handleNewLocation(Location location) {

        mLocation = location;

        Log.i("TEST handleNewLocation", "Called handleNewLocation");
        // Returns the current adapter in use by the ListView supplied (mListView).
        LocationsAdapter adapter = (LocationsAdapter) mListView.getAdapter();
        adapter.setCurrentLocation(location);
        adapter.notifyDataSetChanged();
    }

In each Fragment, I added this code in the onCreateView() below...

Locations fragment example:

Locations fragment;
        if (savedInstanceState != null) {
            fragment = (Locations) getFragmentManager()
                    .findFragmentByTag("locationsFragment");
        } else {
            fragment = new Locations();
            fragment.setArguments(getActivity().getIntent().getExtras());
            getFragmentManager()
                    .beginTransaction()
                    .add(android.R.id.content, fragment, "locationsFragment")
                    .commit();
        }

I also created a method with which to call in the Fragments so that they can receive the Location information that the MainActivity has gathered:

public void getLocation(Location location) {
        mLocation = location;
    }

Which I then call in the Fragments as ((MainActivity)getActivity()).getLocation(mLocation);

So running this code, I get NullPointerException: Attempt to invoke virtual method 'void com.example.android.visitri.Locations.handleNewLocation(android.location.Location)' on a null object reference

Any ideas? I also am not sure that my code is in proper order at all - probably not... Any thoughts on proper implementation? Thanks so much!

  • 1) Move all location related code to the activity 2) If you need to, you can use `setOffscreenPageLimit(3)` to make the ViewPager keep all 4 Fragments initialized: https://developer.android.com/reference/android/support/v4/view/ViewPager.html#setOffscreenPageLimit(int) – Daniel Nugent Nov 29 '17 at 21:11
  • Hello Daniel, thanks for your tips! `setOffscreenPageLimit(3)` works like a charm! I am curious though how I would make reference back to `mListView.getAdapter()` if I were to move that code, as well as all other Location-based code, from my 2 fragments to the MainActivity? I feel like the answer may be fairly simple, but I don't have much experience with accessing data from other activities/fragments yet. – ArclightOne Nov 29 '17 at 21:53
  • Just get the location in the activity, and call into the activity from the fragments in order to get the most current location. Interface callbacks from the fragments to the activity would be a clean way to do that part, but you could also just call into the activity directly, see here for more info: https://stackoverflow.com/questions/12659747/call-an-activity-method-from-a-fragment – Daniel Nugent Nov 29 '17 at 22:41
  • Ok, great! I'll look into that, thanks for your help! I'll give you an update once I've done it - just in case I run into any issues. – ArclightOne Nov 30 '17 at 00:00
  • @DanielNugent So I tried implementing the methods, but ran into some roadblocks due to my inexperience... I hope you have some input? Sorry for being such a grasshopper! :) I detailed the issues in my OP under the large, bolded "UPDATE" text. – ArclightOne Nov 30 '17 at 01:10

0 Answers0