Skip to content

Instantly share code, notes, and snippets.

@9re
Created December 6, 2011 16:53
Show Gist options
  • Save 9re/1438929 to your computer and use it in GitHub Desktop.
Save 9re/1438929 to your computer and use it in GitHub Desktop.
On MapView and the Support Package

On MapView and the Support Package

Introduction

As you may know, google released the support package, which exposed the new APIs only available for higher API level such as Fragment to make it also available for older platforms back to API level 4.

Using the support package, android.support.v4.app.FragmentManager, which is an android.app.FragmentManager compat, is available via FragmentActivity. This means, you must override FragmentActivity to use Fragment APIs.

Multiple Inheritance

These APIs which force the user to override some specific class has a disadvantage: You need some tricks to combine two APIs which both require overriding different classes unless the language you choose allow multiple inheritance. In such cases, perhaps you can avoid multiple inheritance by writing mixin style.

In case of using MapView in Fragment, you must override both MapActivity and FragmentActivity, which is impossible in Java.

A simple solution is: make FragmentActivity extend MapActivity.(android-support-v4-googlemaps does this.) This is a simple and safe fix in that - the diff from original is very small. But this means FragmentActivity also has restrictions which MapActivity has.

Restrictions of MapActivity

As documented here: Only one MapActivity is supported per process. So if you are to plan using multiple FragmentActivities, this restriction is really bad. Or otherwise, you can make app with a single FragmentActivity and implement all screens not with Activities but with Fragments.

Running Multiple MapActivities May Cause Leakage of IntentReceiver

For whom noticed the caveats in javadoc, this section may be useless. But I thought this would be useful for who wondering this weird error message:

E/ActivityThread( 1543): Activity com.example.android.apis.view.MapViewDemo has leaked IntentReceiver com.google.android.maps.NetworkConnectivityListener$ConnectivityBroadcastReceiver@333aa670 that was originally registered here. Are you missing a call to unregisterReceiver()?
E/ActivityThread( 1543): android.app.IntentReceiverLeaked: Activity com.example.android.apis.view.MapViewDemo has leaked IntentReceiver com.google.android.maps.NetworkConnectivityListener$ConnectivityBroadcastReceiver@333aa670 that was originally registered here. Are you missing a call to unregisterReceiver()?
E/ActivityThread( 1543): at android.app.ActivityThread$PackageInfo$ReceiverDispatcher.(ActivityThread.java:953)
E/ActivityThread( 1543): at android.app.ActivityThread$PackageInfo.getReceiverDispatcher(ActivityThread.java:748)
E/ActivityThread( 1543): at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:791)
E/ActivityThread( 1543): at android.app.ContextImpl.registerReceiver(ContextImpl.java:778)
E/ActivityThread( 1543): at android.app.ContextImpl.registerReceiver(ContextImpl.java:772)
E/ActivityThread( 1543): at android.content.ContextWrapper.registerReceiver(ContextWrapper.java:318)
E/ActivityThread( 1543): at com.google.android.maps.NetworkConnectivityListener.startListening(MapActivity.java:163)
E/ActivityThread( 1543): at com.google.android.maps.MapActivity.onResume(MapActivity.java:431)
E/ActivityThread( 1543): at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1149)
E/ActivityThread( 1543): at android.app.Activity.performResume(Activity.java:3823)
E/ActivityThread( 1543): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3211)
E/ActivityThread( 1543): at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3236)
E/ActivityThread( 1543): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
E/ActivityThread( 1543): at android.os.Handler.dispatchMessage(Handler.java:99)
E/ActivityThread( 1543): at android.os.Looper.loop(Looper.java:123)
E/ActivityThread( 1543): at android.app.ActivityThread.main(ActivityThread.java:4735)
E/ActivityThread( 1543): at java.lang.reflect.Method.invokeNative(Native Method)
E/ActivityThread( 1543): at java.lang.reflect.Method.invoke(Method.java:521)
E/ActivityThread( 1543): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
E/ActivityThread( 1543): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:625)
E/ActivityThread( 1543): at dalvik.system.NativeStart.main(Native Method)

An Attempt to Break Down FragmentActivity to Allow Mixin Style

I made a branch experimental-fragment-activity-feature-impl to the android-support-v4-googlemaps project.

The Implementation Strategy

First, I categorized methods of FragmentActivity into two categories. One is inherited methods, and the other is original methods. For the original methods, I made a new interface FragmentActivityFeature and categorized as the features which are expected by other classes. The fields accessed by other classes are also included as their getter methods in the interface. Here is the list:

/***
 * methods from the original FragmentActivity
 ***/
Object onRetainCustomNonConfigurationInstance();
Object getLastCustomNonConfigurationInstance();
void supportInvalidateOptionsMenu();
void onAttachFragment(Fragment fragment);
FragmentManager getSupportFragmentManager();
void startActivityFromFragment(Fragment fragment, Intent intent, int requestCode);
void invalidateSupportFragmentIndex(int index);
LoaderManager getSupportLoaderManager();
LoaderManagerImpl getLoaderManager(int index, boolean started, boolean create);

/*** * getter for the fields of original FragmentActivity * which are accessed internally in the package. ***/ FragmentManagerImpl<?> getFragmentManagerImpl(); boolean isRetaining(); Handler getHandler();

Second, I replaced all the occurrence of FragmentActivity with FragmentActivityFeature and if there is an another requirement: the type also should be an Activity, I repleace the occurrence as parametrized type <FragmentActivityImpl extends Activity & FragmentActivityFeature>. This change made both FragmentManagerImpl and Fragment be a parametrized type.

But making public class like Fragment as a parameterized type is really ugly and uncomfortable, since it will change the usage and you cannot use it like a real one (android.app.Fragment) any more.

Fragment<?> fragment = new Fragment<?>();

It's sure that you can remove the wildcard type parameter here but you'll be warned by the compiler.

So I stopped using parameterized type <FragmentActivityImpl extends Activity & FragmentActivityFeature> and declaired the field mActivity in the class Fragment as an Activity. To keep the type safety, I make the field private and replaced all assignments with setter methods with the signature:

<FragmentActivityImpl extends Activity & FragmentActivityFeature>
void setActivity(FragmentActivityImpl activity) {
    mActivity = activity;
}

This will keep the type safety and you can always cast like

((FragmentActivityFeature)mActivity).getSupportLoaderManager();

safely.

The Bug in JDK 6

For the getter, I declaired

@SuppressWarnings("unchecked")
final public <FragmentActivityImpl extends Activity & FragmentActivityFeature>
FragmentActivityImpl getActivity() {
    return (FragmentActivityImpl) mActivity;
}

But you cannot call this method in older versions of JDK 6 as described here.

You can compile this without any problem with eclipse and JDK 7. So I think this is just a bug.

So I also made a patch, which makes the getter have more simple signature

Activity getActivity() {
    return mActivity;
}

To use the features from FragmentActivityFeature, you can use the same cast,

((FragmentActivityFeature)getActivity()).getSupportLoaderManager();

which is used for the field mActivity internally as described above.

Here, I admit there will be some arguments - make the patched version as default and leave the experimental feature as the patch.


Make Proxy Class or Not

I did not make a proxy class to implement the ActivityFragmentFeature. Instead, I just copied FragmentActivity as FragmentMapActivity and made it extend MapActivity.

Although this may not seems to be prefereble, writing a proxy class is really hard in that the order of calling super method may affect its behaviour.

/**
 * Perform initialization of all fragments and loaders.
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
    mFragments.attachActivity(this);
    // Old versions of the platform didn't do this!
    if (getLayoutInflater().getFactory() == null) {
        getLayoutInflater().setFactory(this);
    }
<b><font color="#0000FF">super</font></b><font color="#990000">.</font><b><font color="#000000">onCreate</font></b><font color="#990000">(</font>savedInstanceState<font color="#990000">);</font>

<font color="#008080">NonConfigurationInstances</font> nc <font color="#990000">=</font> <font color="#990000">(</font>NonConfigurationInstances<font color="#990000">)</font>
        <b><font color="#000000">getLastNonConfigurationInstance</font></b><font color="#990000">();</font>
<b><font color="#0000FF">if</font></b> <font color="#990000">(</font>nc <font color="#990000">!=</font> <b><font color="#0000FF">null</font></b><font color="#990000">)</font> <font color="#FF0000">{</font>
    mAllLoaderManagers <font color="#990000">=</font> nc<font color="#990000">.</font>loaders<font color="#990000">;</font>
<font color="#FF0000">}</font>
<b><font color="#0000FF">if</font></b> <font color="#990000">(</font>savedInstanceState <font color="#990000">!=</font> <b><font color="#0000FF">null</font></b><font color="#990000">)</font> <font color="#FF0000">{</font>
    <font color="#008080">Parcelable</font> p <font color="#990000">=</font> savedInstanceState<font color="#990000">.</font><b><font color="#000000">getParcelable</font></b><font color="#990000">(</font>FRAGMENTS_TAG<font color="#990000">);</font>
    mFragments<font color="#990000">.</font><b><font color="#000000">restoreAllState</font></b><font color="#990000">(</font>p<font color="#990000">,</font> nc <font color="#990000">!=</font> <b><font color="#0000FF">null</font></b> <font color="#990000">?</font> nc<font color="#990000">.</font>fragments <font color="#990000">:</font> <b><font color="#0000FF">null</font></b><font color="#990000">);</font>
<font color="#FF0000">}</font>
mFragments<font color="#990000">.</font><b><font color="#000000">dispatchCreate</font></b><font color="#990000">();</font>

}

This code is taken from FragmentActivity. Is it safe to move the line super.onCreate(savedInstanceState) to the first or to the end of the method? Should the code like

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

or

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

would work? In general, it's not. So I preferred the more simple way.

Common Pitfalls When Using MapView in Fragment

MapActivity can contain only single MapView

This means, MapView should not created more than once in a MapActivity. Do not create new instance at Fragment's onCreateView. So If you once create the instance, hold its reference and re-use it until the Activity's onDestroy.

TouchEvent is intercepted when using a MapView in a ViewPager

ViewPager intercepts TouchEvents by overriding ViewGroup.onInterceptTouchEvent. So if you want to dispatch TouchEvents to the MapView, override this method.

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (shouldDispatchTouchEventsToTheMapView()) {
        return false;
    }
    return super.onInterceptTouchEvent(event);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment