I have an android app that consists of a number of nested fragments, for example I have a set of tabs and clicking a tab loads a fragment into the content area.
One of my tabs is a map showing locations on it and I am doing a geo lookup on a postcode to get back the coordinates for it.
This is all working and now finally I need to ensure that if a user clicks the map tab and initiates the async task which does the geo lookup that a device rotation doesnt stop this.
My map fragment which is loaded when the tab is clicked:
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = (View)inflater.inflate(R.layout.fragment_locations, container, false);
// Make sure user's device supports Google play services
try {
MapsInitializer.initialize(getActivity());
mapView = (MapView) rootView.findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
googleMap = mapView.getMap();
Log.i(LOG_TAG, "Google play service is available.");
if(googleMap == null) {
Toast.makeText(getActivity().getApplicationContext(), "Problem creating Google map", Toast.LENGTH_LONG).show();
} else {
googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
googleMap.getUiSettings().setZoomControlsEnabled(true);
FragmentManager fm = getFragmentManager();
mRetainedTaskFragment = (LocationNetworkOperationsRetainedFragment) fm.findFragmentByTag(TAG_RETAINED_TASK_FRAGMENT);
// If the Fragment is non-null, then it is currently being
// retained across a configuration change.
if (mRetainedTaskFragment == null) {
mRetainedTaskFragment = new LocationNetworkOperationsRetainedFragment();
fm.beginTransaction().add(mRetainedTaskFragment, TAG_RETAINED_TASK_FRAGMENT).commit();
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "Google play service is not available.");
Toast.makeText(getActivity().getApplicationContext(), "Google play service is not available", Toast.LENGTH_LONG).show();
}
return rootView;
}
My async task:
public class LocationNetworkOperations extends AsyncTask<String, Void, LatLng> {
#Override
protected LatLng doInBackground(String... postcodes) {
Geocoder geocoder = new Geocoder(getActivity().getApplicationContext());
LatLng coords= null;
try {
List<Address> addresses = geocoder.getFromLocationName(postcodes[0], 1);
if (addresses != null && !addresses.isEmpty()) {
Address address = addresses.get(0);
coords = new LatLng(address.getLatitude(), address.getLongitude());
} else {
Toast.makeText(getActivity().getApplicationContext(), "Unable to geocode postcode" + postcodes[0], Toast.LENGTH_LONG).show();
}
} catch(Exception ioe) {
ioe.printStackTrace();
}
return coords;
}
#Override
protected void onPostExecute(LatLng coords) {
if(coords != null) {
Marker mapMarkerHome = googleMap.addMarker(new MarkerOptions().position(coords).title("Home")
.snippet("E10 6JQ").icon(BitmapDescriptorFactory
.fromResource(R.drawable.home_map_marker)));
CameraPosition cameraPosition = new CameraPosition.Builder().target(coords).zoom(15).build();
googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
}
#Override
protected void onPreExecute() {}
#Override
protected void onProgressUpdate(Void... values) {}
}
The retained fragment:
public class LocationNetworkOperationsRetainedFragment extends Fragment {
interface LocationNetworkOperationsTaskCallbacks {
void onPreExecute();
void onProgressUpdate(int percent);
void onCancelled();
void onPostExecute();
}
private LocationNetworkOperationsTaskCallbacks mCallbacks;
private LocationsFragment.LocationNetworkOperations mTask;
#Override
public void onAttach(Context context) {
super.onAttach(context);
mCallbacks = (LocationNetworkOperationsTaskCallbacks) context;
}
/**
* This method will only be called once when the retained
* Fragment is first created.
*/
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this fragment across configuration changes.
setRetainInstance(true);
// Create and execute the background task.
mTask = new LocationsFragment().new LocationNetworkOperations();
mTask.execute("E106JQ");
}
#Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
}
So I followed a tutorial and wrote a retained fragment and kick off the aysnc task in that but I am running into the following exception:
java.lang.IllegalStateException: Can't retain fragements that are nested in other fragments
at android.support.v4.app.Fragment.setRetainInstance(Fragment.java:820)
at com.example.android.LocationNetworkOperationsRetainedFragment.onCreate(LocationNetworkOperationsRetainedFragment.java:37)
at android.support.v4.app.Fragment.performCreate(Fragment.java:1939)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:988)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1207)
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:738)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1572)
at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:493)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Why is it not possible to retain the fragment if it is nested and is there a solution to this?
Try using FragmentManager.saveFragmentInstanceState(Fragment) to retrieve a fragment state. The return value implements Parcelable, so you can put it in a Bundle.
For restoration, you can provide the state after creating the fragment using Fragment.setInitialSavedState(Fragment.SavedState).
Can't retain fragements that are nested in other fragments
This is a limitation of nested Fragments. I'm guessing one or more of your child Fragments have setRetainInstance(true) somewhere in their code. You need to remove that to prevent the error.
Related
I am using retrofit to fetch data online.However I cannot find any solution to my problem. I want to call another fragment and display more of the details about the marker I clicked and pass those values to another fragment. Can someone please help me on this. Any help and solutions are well appreciated.
MapFragment.jav
RestAdapter adapter = new RestAdapter.Builder()
.setEndpoint(getString(R.string.fine_dinings))
.build();
RestaurantPlacesApiInterface restaurantPlacesApiInterface =
adapter.create(RestaurantPlacesApiInterface.class);
restaurantPlacesApiInterface.getStreams(new Callback<List<RestaurantPlaces>>() {
#Override
public void success(List<RestaurantPlaces> restaurantPlaces, Response response) {
for (RestaurantPlaces restaurantPlace : restaurantPlaces){
MarkerOptions options = new MarkerOptions().position(new LatLng(restaurantPlace.getLatitude(),
restaurantPlace.getLongitude()));
options.title(restaurantPlace.getName());
options.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));
getMap().addMarker(options);
}
}
#Override
public void failure(RetrofitError error) {
}
});
}
You need to use this:
#Override
public boolean onMarkerClick(Marker marker) {
// call fragment and pass data.
return false;
}
If you return false the click is not consumed.
If you need help implementing this let me know, it's fairly simple.
Here is a quick sample, please change the names to match your own code:
public class MapActivity implements OnMapReadyCallback, GoogleMap.OnMarkerClickListener {
private GoogleMap mGoogleMap;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map);
initMap();
}
public void initMap() {
MapFragment map = (MapFragment) getFragmentManager().findFragmentById(R.id.mapFragment);
map.getMapAsync(this);
}
#Override
public void onMapReady(GoogleMap googleMap) {
try {
if (googleMap != null) {
mGoogleMap = googleMap;
mGoogleMap.setOnMarkerClickListener(this);
mGoogleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
// Now make your retrofit call
}
} catch (Exception e) {
e.printStackTrace();
Log.e("ERROR", "GOOGLE MAPS NOT LOADED");
}
}
#Override
public boolean onMarkerClick(Marker marker) {
Bundle bundle = new Bundle();
bundle.putString("myString", "value");
// set Fragment class Arguments
MyFragment myFragment= new MyFragment();
myFragment.setArguments(bundle);
// launch fragment
return false;
}
}
better to use interface in fragment and implement that interface in (activity where RestAdapter used)
I've got an app which is quite intensive in terms of memory and i'm trying to get all I can. I've noticed something which I can't work out, Google maps is keeping its allocation of memory even after (I think) i'm getting rid of it.
Before Google Maps is called:
After Google Maps is called:
Returned from Google Maps with back button:
Returned from Google Maps with on info window press button:
(the slightly higher number is just due to the fact I included a couple of methods more to include a marker to return)
As you can see, it's retaining a lot of the memory. So i'll show you how this is working:
Google Maps is not actually called, but a container for Google Maps is, and it's below:
public class GoogleMapsAndBookmarksContainer extends ActionBarActivity {
private FragmentTabHost mTabHost;
public ArrayList frameListContainer;
public HashMap<String, HashMap> bookmarkInfo;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_google_maps_and_bookmarks_container);
frameListContainer = (ArrayList) getIntent().getSerializableExtra("arrayListWithFrameAttributes");
bookmarkInfo = (HashMap) getIntent().getSerializableExtra("hashmapWithBookmarks");
mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost);
mTabHost.setup(this, getSupportFragmentManager(), android.R.id.tabcontent);
// The first tab is the google maps fragment which i'll include below
mTabHost.addTab(
mTabHost.newTabSpec("tab1").setIndicator("Google Maps", null),
GoogleMapsFragment.class, null);
mTabHost.addTab(
mTabHost.newTabSpec("tab2").setIndicator("Bookmarks", null),
BookmarksFragment.class, null);
}
#Override
protected void onDestroy() {
super.onDestroy();
/// **** THIS WAS JUST ADDED A LOSS FOR WHAT IT COULD BE RETAINING ****
mTabHost = null;
frameListContainer = null;
bookmarkInfo = null;
}
// access for fragments
public HashMap getBookmarkInfo(){
return bookmarkInfo;
}
public ArrayList getFrameAttributesArrayList(){
return frameListContainer;
}
}
I managed to remove everything from my class but this and still the problem remain:
public class GoogleMapsFragment extends Fragment {
private GoogleMap map;
private SupportMapFragment mapFragment;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_google_maps, container, false);
FragmentManager fm = getChildFragmentManager();
mapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map);
mapFragment.getMapAsync(new OnMapReadyCallback() {
public void onMapReady(GoogleMap googleMap) {
map = googleMap;
initMap();
}
});
return view;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// frameListContainer = ((GoogleMapsAndBookmarksContainer) this.getActivity()).getFrameAttributesArrayList();
// bookmarkInfo = ((GoogleMapsAndBookmarksContainer) this.getActivity()).getBookmarkInfo();
// markerToFrameAndRoute = new HashMap<Marker, String[]>();
}
#Override
public void onDestroy() {
super.onDestroy();
mapFragment = null;
System.gc();
map = null; // I did add this in an edit, but it was mistakenly taken away when I was cutting it up, sorry
}
}
So, is this solvable? I can't work out what it is that it's retaining. I did notice if I keep going back on Google Maps and off it, it is garbage collecting it, so it's not a memory leak.
edit: have tried map.clear(); still keeps the memory
Just clear map
#Override
public void onDestroy() {
super.onDestroy();
map.clear();
}
One probable solution to this problem may be to use a retained fragment using setRetainInstance(boolean retain). In this process the map fragment wont destroy and recreate itself dumping a lot of memory, instead it will just retain the memory when it is needed, when you switch map fragment back and forth (by pressing back button).
public class RetainMapActivity extends FragmentActivity {
private GoogleMap mMap;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.basic_demo);
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
if (savedInstanceState == null) {
// First incarnation of this activity.
mapFragment.setRetainInstance(true);
} else {
// Reincarnated activity. The obtained map is the same map instance in the previous
// activity life cycle. There is no need to reinitialize it.
mMap = mapFragment.getMap();
}
setUpMapIfNeeded();
}
#Override
protected void onResume() {
super.onResume();
setUpMapIfNeeded();
}
private void setUpMapIfNeeded() {
if (mMap == null) {
mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map))
.getMap();
if (mMap != null) {
setUpMap();
}
}
}
private void setUpMap() {
mMap.addMarker(new MarkerOptions().position(new LatLng(0, 0)).title("Marker"));
}
Another solution to this may be to use a Otto event bus which is designed to decouple different parts of your application while still allowing them to communicate efficiently.
Hope this Helps!!
I'm having an issue with my Android app. I'm using navigation drawer with onCreateView() inside of a PlaceholderFragment like this:
// Google Map
private GoogleMap map;
private final LatLng LOCATION_DEPAUW = new LatLng(39.640343, -86.860687);
public static class PlaceholderFragment extends Fragment {
/**
* Returns a new instance of this fragment for the given section number.
*/
public static PlaceholderFragment newInstance(int sectionNumber) {
PlaceholderFragment fragment = new PlaceholderFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return fragment;
}
public PlaceholderFragment() {
}
#SuppressWarnings("unused")
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = null;
...
else if(mTitle.equals("Map")){
Log.d("Map", "Worked");
rootView = inflater.inflate(R.layout.google_map, container, false);
try {
if (map == null) {
SupportMapFragment fm = (SupportMapFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.map);
map = fm.getMap();
}
// Needs to call MapsInitializer before doing any CameraUpdateFactory calls
try {
MapsInitializer.initialize(this.getActivity());
} catch (GooglePlayServicesNotAvailableException e) {
e.printStackTrace();
}
CameraUpdate update = CameraUpdateFactory.newLatLngZoom(LOCATION_DEPAUW, 15);
map.setMapType(GoogleMap.MAP_TYPE_NORMAL);
// Enabling MyLocation Layer of Google Map
map.setMyLocationEnabled(true);
map.getUiSettings().setZoomGesturesEnabled(true);
map.getUiSettings().setCompassEnabled(true);
map.getUiSettings().setRotateGesturesEnabled(true);
map.animateCamera(update);
/*
* for different colors:
* googlemap.addMarker(new MarkerOptions().position(new LatLng( 65.07213,-2.109375)).title("This is my title").snippet("and snippet").icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE)));
*
* .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE) is the key. change the HUE_ORANGE to anything that is available
*
*/
} catch (Exception e) {
Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_LONG).show();
}
}
I have already imported everything correctly and the map initializes correctly on the first view. The problem is when I navigate to another fragment and then go back to this map fragment, my app crashes. I've tried to use an eclipse emulator but couldnt get it configured correctly. Does anyone know a way to handle the life cycle of a map inside a placeholderfragment? Thanks!
So after a lot of research, I found out someone manage to handle this well.
Check out this github repository:
https://gist.github.com/joshdholtz/4522551
MapView mapView;
GoogleMap map;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.some_layout, container, false);
// Gets the MapView from the XML layout and creates it
mapView = (MapView) v.findViewById(R.id.mapview);
mapView.onCreate(savedInstanceState);
// Gets to GoogleMap from the MapView and does initialization stuff
map = mapView.getMap();
map.getUiSettings().setMyLocationButtonEnabled(false);
map.setMyLocationEnabled(true);
// Needs to call MapsInitializer before doing any CameraUpdateFactory calls
try {
MapsInitializer.initialize(this.getActivity());
} catch (GooglePlayServicesNotAvailableException e) {
e.printStackTrace();
}
// Updates the location and zoom of the MapView
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(new LatLng(43.1, -87.9), 10);
map.animateCamera(cameraUpdate);
return v;
}
#Override
public void onResume() {
mapView.onResume();
super.onResume();
}
#Override
public void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
#Override
public void onLowMemory() {
super.onLowMemory();
mapView.onLowMemory();
}
initially though, I had issues cause I had multiple fragments to switch between, so i did this in mine:
public void onResume() {
try{
super.onResume();
mapView.onResume();
}catch(NullPointerException e){
Log.d("onResume", "NullPointerException: " + e);
}
}
#Override
public void onDestroy() {
try{
super.onDestroy();
mapView.onDestroy();
}catch(NullPointerException e){
Log.d("onDestroy", "NullPointerException: " + e);
}
}
#Override
public void onLowMemory() {
try{
super.onLowMemory();
mapView.onLowMemory();
}catch(NullPointerException e){
Log.d("onLowMemory", "NullPointerException: " + e);
}
}
I have two FrameLayout in an activity, I just replace fragments inside those FrameLayout.
It works great with everyfragment BUT the one containing the MapFragment for the GoogleMap.
When I first launch the app it works and I see the map (everything works) but when I replace the fragment and replace it back I just get a blank space like nothing is loaded.
public class MapFragment extends CustomFragment {
private GoogleMap mMap;
private Marker currentMarker;
private OnFragmentInteractionListener mListener;
public static final String ID = "MAP_FRAGMENT";
private static View view;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = null;
try {
view = inflater.inflate(R.layout.fragment_map, container, false);
} catch (InflateException e) {
Log.d("","");
}finally{
setUpMapIfNeeded();
if(view!=null)this.view=view;
return this.view;
}
}
#Override
public void onDestroyView() {
super.onDestroyView();
Fragment f = getFragmentManager().findFragmentById(R.id.map);
if (f != null)getActivity().getFragmentManager().beginTransaction().remove(f).commit();
mMap=null;
currentMarker=null;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnFragmentInteractionListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
private void setUpMapIfNeeded() {
// Do a null check to confirm that we have not already instantiated the map.
if (mMap == null) {
// Try to obtain the map from the SupportMapFragment.
mMap = ((com.google.android.gms.maps.MapFragment)getFragmentManager().findFragmentById(R.id.map)).getMap();
// Check if we were successful in obtaining the map.
if (mMap != null) {
setUpMap();
}
}
}
private void setUpMap() {
}
#Override
public void onPause() {
super.onPause();
}
#Override
public void update(FragmentMessage message) {
if(mMap!=null) {
Location loc = (Location) message.getMessages()[0];
if (currentMarker != null)
currentMarker.remove();
currentMarker = mMap.addMarker(new MarkerOptions().position(new LatLng(loc.getLatitude(), loc.getLongitude())).title("Marker"));
CameraPosition cameraPosition = new CameraPosition.Builder()
.target(new LatLng(loc.getLatitude(), loc.getLongitude())) // Sets the center of the map to Mountain View
.zoom(15) // Sets the zoom
.build(); // Creates a CameraPosition from the builder
mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
}
}
The mMap is never null, so the MapFragment is working I guess but I don't get anything on the screen (see screenshots below).
Does anyone have an idea ? I really don't know what is wrong.
I have a layout with ActionBar.NAVIGATION_MODE_TABS. The first tab is to display the results in listview and the second in a map. Everything worked great until I have upgraded to the latest version of Google Play Services lib.
Here the fragment used to display the map:
public class PlaceMapFragment extends Fragment {
MapView mapView;
GoogleMap map;
ResultsListener mCallback;
List<Place> places;
private HashMap<String, Place> placeMarker = new HashMap<String, Place>();
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
places = mCallback.getPlaces();
View rootView = inflater.inflate(R.layout.mapview, container, false);
mapView = (MapView) rootView.findViewById(R.id.mapview);
mapView.onCreate(savedInstanceState);
map = mapView.getMap();
setUpMap();
return rootView;
}
private void setUpMap() {
map.getUiSettings().setMyLocationButtonEnabled(true);
map.setMyLocationEnabled(true);
/*
try {
MapsInitializer.initialize(this.getActivity());
} catch (GooglePlayServicesNotAvailableException e) {
e.printStackTrace();
}
*/
// add markers
}
#Override
public void onResume() {
mapView.onResume();
super.onResume();
setUpMap();
}
#Override
public void onPause() {
mapView.onPause();
super.onPause();
}
#Override
public void onDestroy() {
mapView.onDestroy();
super.onDestroy();
}
#Override
public void onLowMemory() {
super.onLowMemory();
mapView.onLowMemory();
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
public interface ResultsListener {
public List<Place> getPlaces();
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (ResultsListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement ResultsListener");
}
}
}
I get these errors:
04-05 03:58:22.040: E/AndroidRuntime(661): FATAL EXCEPTION: main
04-05 03:58:22.040: E/AndroidRuntime(661): java.lang.NullPointerException
04-05 03:58:22.040: E/AndroidRuntime(661): at com.tomsyweb.mapslee.fragments.PlaceMapFragment.setUpMap(PlaceMapFragment.java:54)
04-05 03:58:22.040: E/AndroidRuntime(661): at com.tomsyweb.mapslee.fragments.PlaceMapFragment.onCreateView(PlaceMapFragment.java:48)
I have commented the MapsInitializer.initialize(this.getActivity()); part because GooglePlayServicesNotAvailableException gives error.
mapview.xml
<com.google.android.gms.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mapview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
My code is taken from Getting Google Map Fragment in onCreateView using ActionBar tabs
Your map may be null if the device doesn't have the latest version of Maps installed, or perhaps for other reasons. Trying to create it may even prompt the user to start a different activity to update their device. So it's recommended to create an initMap() method like this, and call it in your onCreate(...) and onResume():
private void initMap() {
if(map == null) {
map = ((MapFragment) getFragmentManager().findFragmentById(R.id.map)).getMap();
if(map != null) {
// set up map
}
}
}