Restoring MapView's state on rotate and on back

29,544

Solution 1

Here is my fix to the first problem:

It seems that Map is trying to unparcel all the bundle, not just it's own information when I call mMap.onCreate(savedInstanceState) and it has problem with it if I'm using my custom Parcelable class. The solution that worked for me was removing my extras from savedInstanceState as soon as I used them - before I call Map's onCreate(). I do it with savedInstanceState.remove(MY_KEY). Another thing I had to do was to call mMap.onSaveInstanceState() before adding my own information to outState in Fragment's onSaveInstanceState(Bundle outState) function.

And here's how I handled the second one:

I simplified the example project to the bare bones. I was adding raw Markers to the map and if I replace the Fragment with map with another one then after clicking "back" I still got "nulled" map. So I did two things:

  1. saving CameraPosition in onPause() function to restore it in onResume()
  2. setting mMap to null in onPause() so when the Fragment comes back, the Markers are added again by the addMapPoints() function (I had to change it a little bit since I was saving and checking Markers id's).

Here are code samples:

private CameraPosition cp;

...

public void onPause() {
    mMapView.onPause();
    super.onPause();

    cp = mMap.getCameraPosition();
    mMap = null;
}

...

public void onResume() {
    super.onResume();
    setUpMapIfNeeded();
    mMapView.onResume();
    if (cp != null) {
        mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cp));
        cp = null;
    }
}

And to update the camera position in onResume() I had to manually initialize maps. I did it in setUpMap():

private void setUpMap() {
    try {
        MapsInitializer.initialize(getActivity());
    } catch (GooglePlayServicesNotAvailableException e) {
    }
    mMap.setOnInfoWindowClickListener(this);
    mMap.setOnMapLongClickListener(this);
    addMapPoints();
}

I realize that those aren't real solutions - just overrides but it's the best I can do for now and the project must go on. If anyone finds cleaner fixes I'll be grateful for letting me know about them.

Solution 2

Came here in search of any hints regarding onSaveInstance and NullPointerException. I have tried various workarounds including the one mentioned by @Izydorr but have had no luck. The issue was with FragmentPagerAdapter - the adapter of the ViewPager that was hosting my fragments that were embedding the MapViews. After having it changed to FragmentStatePagerAdapter the NPEs have gone away. Whew!

Share:
29,544

Related videos on Youtube

Izydorr
Author by

Izydorr

Updated on July 09, 2022

Comments

  • Izydorr
    Izydorr almost 2 years

    Background

    I have a larger application in which I had/have several problems with new Google Maps API. I tried to describe it in a different question but since it seems too complex I decided to start a new project, as simple as possible and try to reproduce problems. So here it is.

    The situation

    I'm using Fragments and want to put MapView inside. I don't want to use MapFragment. The sample project I prepared may be not very beautiful but I tried to make it as simple as possible and it had to contain some elements (again simplified) from the original app. I have one Activity and my custom Fragment with MapView in it, added programatically. The Map contains some points/Markers. After clicking on a Marker the InfoWindow is shown and clicking on it causes next Fragment being shown (with replace() function) in content.

    The problems

    There are two issues I have:

    1. When the Map with Markers is displayed screen rotation causes Class not found when unmarshalling error with my custom MyMapPoint class - I have no idea why and what it means.

    2. I click the Marker and then InfoWindow. After this I press hardware back button. Now I can see the Map but with no Markers and centered in 0,0 point.

    The code

    MainActivity

    public class MainActivity extends FragmentActivity {
    
        private ArrayList<MyMapPoint> mPoints = new ArrayList<MyMapPoint>();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);     
    
            if (savedInstanceState == null) {
                mPoints.add(new MyMapPoint(1, new LatLng(20, 10), 
                    "test point", "description", null));            
                mPoints.add(new MyMapPoint(2, new LatLng(10, 20), 
                    "test point 2", "second description", null));
    
                Fragment fragment = MyMapFragment.newInstance(mPoints);
                getSupportFragmentManager().beginTransaction()
                    .add(R.id.contentPane, fragment).commit();
            }
        }
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/contentPane"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
    

    map_fragment.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <com.google.android.gms.maps.MapView
            xmlns:map="http://schemas.android.com/apk/res-auto"
            android:id="@+id/map"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>
    

    MyMapFragment

    public class MyMapFragment extends Fragment implements
        OnInfoWindowClickListener {
    
        public static final String KEY_POINTS = "points";
    
        private MapView mMapView;
        private GoogleMap mMap;
        private HashMap<MyMapPoint, Marker> mPoints = 
            new HashMap<MyMapPoint, Marker>();
    
        public static MyMapFragment newInstance(ArrayList<MyMapPoint> points) {
            MyMapFragment fragment = new MyMapFragment();
            Bundle args = new Bundle();
            args.putParcelableArrayList(KEY_POINTS, points);
            fragment.setArguments(args);
            return fragment;
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            mMapView.onSaveInstanceState(outState);
            MyMapPoint[] points = mPoints.keySet().toArray(
                new MyMapPoint[mPoints.size()]);
            outState.putParcelableArray(KEY_POINTS, points);
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            if (savedInstanceState == null) {
                Bundle extras = getArguments();
                if ((extras != null) && extras.containsKey(KEY_POINTS)) {
                    for (Parcelable pointP : extras.getParcelableArrayList(KEY_POINTS)) {
                        mPoints.put((MyMapPoint) pointP, null);
                    }
                }
            } else {
                MyMapPoint[] points = (MyMapPoint[]) savedInstanceState
                    .getParcelableArray(KEY_POINTS);
                for (MyMapPoint point : points) {
                    mPoints.put(point, null);
                }
            }
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, 
            ViewGroup container, Bundle savedInstanceState) {
            View layout = inflater.inflate(R.layout.map_fragment, container, false);
            mMapView = (MapView) layout.findViewById(R.id.map);
            return layout;
        }
    
        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            mMapView.onCreate(savedInstanceState);
            setUpMapIfNeeded();
            addMapPoints();
        }
    
        @Override
        public void onPause() {
            mMapView.onPause();
            super.onPause();
        }
    
        @Override
        public void onResume() {
            super.onResume();
            setUpMapIfNeeded();
            mMapView.onResume();
        }
    
        @Override
        public void onDestroy() {
            mMapView.onDestroy();
            super.onDestroy();
        }
    
        public void onLowMemory() {
            super.onLowMemory();
            mMapView.onLowMemory();
        };
    
        private void setUpMapIfNeeded() {
            if (mMap == null) {
                mMap = ((MapView) getView().findViewById(R.id.map)).getMap();
                if (mMap != null) {
                    setUpMap();
                }
            }
        }
    
        private void setUpMap() {
            mMap.setOnInfoWindowClickListener(this);
            addMapPoints();
        }
    
        private void addMapPoints() {
            if (mMap != null) {
                HashMap<MyMapPoint, Marker> toAdd = 
                    new HashMap<MyMapPoint, Marker>();
                for (Entry<MyMapPoint, Marker> entry : mPoints.entrySet()) {
                    Marker marker = entry.getValue();
                    if (marker == null) {
                        MyMapPoint point = entry.getKey();
                        marker = mMap.addMarker(point.getMarkerOptions());
                        toAdd.put(point, marker);
                    }
                }
                mPoints.putAll(toAdd);
            }
        }
    
        @Override
        public void onInfoWindowClick(Marker marker) {
            Fragment fragment = DetailsFragment.newInstance();
            getActivity().getSupportFragmentManager().beginTransaction()
                .replace(R.id.contentPane, fragment)
                .addToBackStack(null).commit();
        }
    
        public static class MyMapPoint implements Parcelable {
            private static final int CONTENTS_DESCR = 1;
    
            public int objectId;
            public LatLng latLng;
            public String title;
            public String snippet;
    
            public MyMapPoint(int oId, LatLng point, 
                String infoTitle, String infoSnippet, String infoImageUrl) {
                objectId = oId;
                latLng = point;
                title = infoTitle;
                snippet = infoSnippet;
            }
    
            public MyMapPoint(Parcel in) {
                objectId = in.readInt();
                latLng = in.readParcelable(LatLng.class.getClassLoader());
                title = in.readString();
                snippet = in.readString();
            }
    
            public MarkerOptions getMarkerOptions() {
                return new MarkerOptions().position(latLng)
                    .title(title).snippet(snippet);
            }
    
            @Override
            public int describeContents() {
                return CONTENTS_DESCR;
            }
    
            @Override
            public void writeToParcel(Parcel dest, int flags) {
                dest.writeInt(objectId);
                dest.writeParcelable(latLng, 0);
                dest.writeString(title);
                dest.writeString(snippet);
            }
    
            public static final Parcelable.Creator<MyMapPoint> CREATOR = 
                new Parcelable.Creator<MyMapPoint>() {
                public MyMapPoint createFromParcel(Parcel in) {
                    return new MyMapPoint(in);
                }
    
                public MyMapPoint[] newArray(int size) {
                    return new MyMapPoint[size];
                }
            };
    
        }
    }
    

    If you need to take a look at any other file - let me know. Here you can find a complete project, you just have to put your own Maps API KEY in AndroidManifest.xml file.

    EDIT

    I managed to make the example even more simple and updated the code above.

  • darnmason
    darnmason almost 10 years
    Cheers for your answer re the Map attempting to unparcel the whole Bundle, I implemented your suggestion ages ago, but recently added a ViewPager to my Fragment which caused the BadParcelableException again. Turns out the ViewPager's saved state can't be unparcelled either as it's from the Support Library so I opted to copy the map state into a new Bundle instead of removing all the parcels I knew about, my answer is here: stackoverflow.com/a/24651246/1007151
  • Ruben2112
    Ruben2112 almost 7 years
    This Interface is now deprecated. See here: stackoverflow.com/questions/38727517/…