Showing posts with label Android. Show all posts
Showing posts with label Android. Show all posts

Thursday, November 6, 2014

Android Studio/IntelliJ false error Object.getClass() shows as ambiguous method call

Tired of seeing this issue? Yes, it's terrible because it creates a lot noise for us to find the real errors. And we don't want to bend our code to remove the redness in the IDE with this hack - ((Object)this).getClass(). Solution: Link into the source code by clicking getClass() in your IDE. We should see the code below
    public final Class<?> getClass() {//Remove <?>
      return shadow$_klass_;
    }
Remove the <?> and refresh the project. The red lines should be gone.
    public final Class getClass() {//<?> removed
      return shadow$_klass_;
    }

Saturday, October 25, 2014

Nested Fragment with ChildFragmentManager lost state in rev20/rev21 of Android support library

After Android support library v4 rev 20 released, our team excitedly try to move on to it. But there was an annoying bug found - after screen rotation the inner nested fragment lost its state!

What we got was a ViewPager with several tabs in the nested fragment. All fragments setRetaininstanceState(true). On Android support lib v4 rev 19.+ everything worked fine. After we jumped on to rev 20, tabs don't display after rotation any more. This issue still exists in rev 21.0.0.

I started scratching my hair about this crazy issue until I found this was caused by the update of the android support library. If we revert back to use rev 19, everything went back fine. But we don't want to be stuck on rev19 forever as new version introduced many other important features.

After blood, sweat and tears, I finally found a work around if we really want to leap forward from support library v4 rev19. By diff of the source code of rev19 and rev20 I found what change results the problem. (the source files for different revisions can be found on your local drive under [AndroidSdkFolder]/extras/android/m2repository/com/android/support/support-v4). If you are interested, you can look into the source code. The change related to the bug is

after rev 20.0.0 there was new line at #1204 in android.support.v4.app.Fragment.java
  mChildFragmentManager = null;
added in method Fragment#initState().

As Fragment#initState() would be called internally by the lib every time the fragment is created regardless it's newly created or recreated, retain instance state or not. In the life cycles of a fragment, if the lib found mChildFragmentManager is not null it will dispatch events to it but as you can see mChildFragmentManager is reset null in rev20, so nothing would happen to the previous child fragment manager after rotation.

Solution:
Ideally, I hope android team could fix this issue ASAP. But before that there is a work around I found work. The idea is we retain the child fragment manager ourselves! To do so, we need to do the below trick to all nested fragments.

1. Create a field in the fragment to keep the child fragment manager created by the lib. As we set retain instance true, the reference held by the field will be kept after the rotation.

2. Before the lib dispatch the life cycle events of the the recreated fragment, we need to set its previous mChildFragmentManger retained by step 1 back to the new created fragment. The hook point is onAttachActivity(). As there is no public accessor we have to use reflection to set the mChildFragmentManger in the fragment. Caveats: reflection may cause problem for the future version because the field name may change.

3. We need to replace the destroyed activity attached to the fragment manager and existing fragments by the latest recreated activity.

Sample code:
public class NestingFragment extends Fragment {
    //...other codes

    private FragmentManager retainedChildFragmentManager;
    private FragmentHostCallback currentHost;
    private Class fragmentImplClass;
    private Field mHostField;

    {
        //Prepare the reflections to manage hiden fileds
        try {
            fragmentImplClass = Class.forName("android.support.v4.app.FragmentManagerImpl");
            mHostField = fragmentImplClass.getDeclaredField("mHost");
            mHostField.setAccessible(true);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("FragmentManagerImpl is renamed due to the " +
                    "change of Android SDK, this workaround doesn't work any more. " +
                    "See the issue at " +
                    "https://code.google.com/p/android/issues/detail?id=74222", e);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException("FragmentManagerImpl.mHost is found due to the " +
                    "change of Android SDK, this workaround doesn't work any more. " +
                    "See the issue at " +
                    "https://code.google.com/p/android/issues/detail?id=74222", e);
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    protected FragmentManager childFragmentManager() {
        if (retainedChildFragmentManager == null) {
            retainedChildFragmentManager = getChildFragmentManager();
        }
        return retainedChildFragmentManager;
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (retainedChildFragmentManager != null) {
            //restore the last retained child fragment manager to the new
            //created fragment
            try {
                //Copy the mHost(Activity) to retainedChildFragmentManager
                currentHost = (FragmentHostCallback) mHostField.get(getFragmentManager());

                Field childFMField = Fragment.class.getDeclaredField("mChildFragmentManager");
                childFMField.setAccessible(true);
                childFMField.set(this, retainedChildFragmentManager);

                refreshHosts(getFragmentManager());
            } catch (Exception e) {
                logger.warn(e.getMessage(), e);
            }
            //Refresh children fragment's hosts
        } else {
            //If the child fragment manager has not been retained yet, let it hold the internal
            //child fragment manager as early as possible. This can prevent child fragment
            //manager from missing to be set and then retained, which could happen when
            //OS kills activity and restarts it. In this case, the delegate fragment restored
            //but childFragmentManager() may not be called so mRetainedChildFragmentManager is
            //yet set. If the fragment is rotated, the state of child fragment manager will be
            //lost since mRetainedChildFragmentManager hasn't set to be retained by the OS.
            retainedChildFragmentManager = getChildFragmentManager();
        }
    }

    private void refreshHosts(FragmentManager fragmentManager) throws IllegalAccessException {
        if (fragmentManager != null) {
            replaceFragmentManagerHost(fragmentManager);
        }

        //replace host(activity) of fragments already added
        List frags = fragmentManager.getFragments();
        if (frags != null) {
            for (Fragment f : frags) {
                if (f != null) {
                    try {
                        //Copy the mHost(Activity) to retainedChildFragmentManager
                        Field mHostField = Fragment.class.getDeclaredField("mHost");
                        mHostField.setAccessible(true);
                        mHostField.set(f, currentHost);
                    } catch (Exception e) {
                        logger.warn(e.getMessage(), e);
                    }
                    if (f.getChildFragmentManager() != null) {
                        refreshHosts(f.getChildFragmentManager());
                    }
                }
            }
        }
    }

    //replace host(activity) of the fragment manager so that new fragments it creates will be attached
    //with correct activity
    private void replaceFragmentManagerHost(FragmentManager fragmentManager) throws IllegalAccessException {
        if (currentHost != null) {
            mHostField.set(fragmentManager, currentHost);
        }
    }

    //...other codes
}




Wednesday, January 15, 2014

Android Activity/Fragment life cycle analysis

Android life cycle is complex when activity comes with fragments. Though the document improves a lot, it still has not covered all scenarios. To have a better understanding of it, I made a simple app to print out all critical life cycles for both activity and nested fragments.

First of all, it is important to point out that when all activities of an app are killed the app process may still be running. As long as the process is running, all static variables will be valid. Once the process is killed by the OS all static variables will be lost. So be careful to use static variables.

To cover all possible scenarios that may impact your android app, I categorised them below
  1. New created: Fragment/Activity starts from nothing
  2. Rotate new created: Fragment/Activity restarts after rotation
  3. Home key pressed: Fragment/Activity is kicked to the background
  4. Back from background
with the combination of different cases:
  1. Fragment sets RetainInstance true/false
  2. App is killed in the background after home key pressed. To simulate the app is killed by the system in the background, you can tick the box under Settings->Developer options->Don't keep activities. In this case, the active activity will be killed even the app is sent to background by pressing home button.

Here is the simple project used for the analysis
https://github.com/kejunxia/AndroidLifeCycleAnalysis

Also here is a library to apply Android MVC pattern and make the lifecycle easier to be used as well
http://kejunxia.github.io/AndroidMvc/
https://github.com/kejunxia/AndroidMvc

Summary:
  • New created: almost the same for the four scenarios as below. Except the sequence of onCreateView and onWindowFocusChanged
  • Rotate new created: When Fragment get set RetainInstance true, fragment doesn't call onCreate and onDestroy. And the following fragment calls are invoked but bundle passed in are null
    • onCreateView
    • onViewCreated
    • onActivityCreated
    • onViewStateRestored
    Also note that onSaveInstanceState for both Activity and Fragment is called with non-null outState, no matter if the fragment is set RetainInstance true or false. Which means activities and fragments always save instance state during rotation.
  • Home button pressed(send app to background): onSaveInstanceState is called with outState always as the result above. When fragment is set RetainInstance true, the following calls won't be called:
    • Activity.onDestroy
    • Fragment.onDestroyView
    • Fragment.onDestroy
    • Fragment.onDetach
    • Activity.onDetachedFromWindow
  •  Back from background: This is a little complicated. I split it in to 2 cases.
    • App killed by system: this is simulated by turning on Don't Keep Activities in developer settings on the device. In this case the system is going to try restoring the previous state which should be stored by onSaveInstanceState(as mentioned it's always called). This is different from creating a new activity. In this case the system will do the things below:
      • Activity.onCreate will be called with the savedInstanceState to recover previous state, while if it's creating a new activity onCreate will recieve a null bundle.
      • Fragment.onAttach
      • Activity.onAttachFragment
      • Activity.onStart NOTE THAT this will be called after the fragment is attached while if it's new creating activity, onstart is before the fragment is attached.
      • Fragment.onCreate will be called with savedInstanceState like the activity.
      • Activity.onRestoreInstanceState is called which won't be called when creating a new activity
    • Back from background before killed:
      • no creation will occur
      • no savedInstanceState will occur

Life cycle outputs:

========================================================================
Activity:                            new created
Fragment retainInstance:   false
Kill Activity immediately:   false
ActivityCycle﹕ onCreate: bundle=null
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=null
FragmentCycle===>﹕ onCreateView: bundle=null
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=null
FragmentCycle===>﹕ onActivityCreated: bundle=null
FragmentCycle===>﹕ onViewStateRestored: bundle=null
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onPostCreate: bundle=null
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments`
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView

Activity:                            rotate new created
Fragment retainInstance:   false
Kill Activity immediately:   false
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop
ActivityCycle﹕ onDestroy
FragmentCycle===>﹕ onDestroyView
FragmentCycle===>﹕ onDestroy
FragmentCycle===>﹕ onDetach
ActivityCycle﹕ onDetachedFromWindow
ActivityCycle﹕ onCreate: bundle=Object
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=Object
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onCreateView: bundle=Object
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=Object
FragmentCycle===>﹕ onActivityCreated: bundle=Object
FragmentCycle===>﹕ onViewStateRestored: bundle=Object
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onRestoreInstanceState: bundle=Object
ActivityCycle﹕ onPostCreate: bundle=Object
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged

Activity:                           home button pressed
Fragment retainInstance:   false
Kill Activity immediately:   false
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop

Activity:                            back from background
Fragment retainInstance:   false
Kill Activity immediately:   false
ActivityCycle﹕ onRestart
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onWindowFocusChanged

========================================================================
Activity:                            new created
Fragment retainInstance:   true
Kill Activity immediately:   false

ActivityCycle﹕ onCreate: bundle=null
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=null
FragmentCycle===>﹕ onCreateView: bundle=null
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=null
FragmentCycle===>﹕ onActivityCreated: bundle=null
FragmentCycle===>﹕ onViewStateRestored: bundle=null
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onPostCreate: bundle=null
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView

Activity:                            rotate new created
Fragment retainInstance:   true
Kill Activity immediately:   false
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop
ActivityCycle﹕ onDestroy
FragmentCycle===>﹕ onDestroyView
FragmentCycle===>﹕ onDetach
ActivityCycle﹕ onDetachedFromWindow
ActivityCycle﹕ onCreate: bundle=Object
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onCreateView: bundle=null
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=null
FragmentCycle===>﹕ onActivityCreated: bundle=null
FragmentCycle===>﹕ onViewStateRestored: bundle=null
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onRestoreInstanceState: bundle=Object
ActivityCycle﹕ onPostCreate: bundle=Object
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged

Activity:                            home button pressed
Fragment retainInstance:   true
Kill Activity immediately:   
false
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop


Activity:                            back from background
Fragment retainInstance:   true
Kill Activity immediately:   
false
ActivityCycle﹕ onRestart
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onWindowFocusChanged

======================================================================
Activity:                            new created
Fragment retainInstance:   true
Kill Activity immediately:   true
ActivityCycle﹕ onCreate: bundle=null
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=null
FragmentCycle===>﹕ onCreateView: bundle=null
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=null
FragmentCycle===>﹕ onActivityCreated: bundle=null
FragmentCycle===>﹕ onViewStateRestored: bundle=null
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onPostCreate: bundle=null
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView


Activity:                            rotate new created
Fragment retainInstance:   true
Kill Activity immediately:   true
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop
ActivityCycle﹕ onDestroy
FragmentCycle===>﹕ onDestroyView
FragmentCycle===>﹕ onDetach
ActivityCycle﹕ onDetachedFromWindow
ActivityCycle﹕ onCreate: bundle=Object
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onCreateView: bundle=null
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=null
FragmentCycle===>﹕ onActivityCreated: bundle=null
FragmentCycle===>﹕ onViewStateRestored: bundle=null
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onRestoreInstanceState: bundle=Object
ActivityCycle﹕ onPostCreate: bundle=Object
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged


Activity:                            home button pressed
Fragment retainInstance:   true
Kill Activity immediately:   true
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop
ActivityCycle﹕ onDestroy
FragmentCycle===>﹕ onDestroyView
FragmentCycle===>﹕ onDestroy
FragmentCycle===>﹕ onDetach
ActivityCycle﹕ onDetachedFromWindow


Activity:                            back from background
Fragment retainInstance:   true
Kill Activity immediately:   true
ActivityCycle﹕ onCreate: bundle=Object
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=Object
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onCreateView: bundle=Object
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=Object
FragmentCycle===>﹕ onActivityCreated: bundle=Object
FragmentCycle===>﹕ onViewStateRestored: bundle=Object
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onRestoreInstanceState: bundle=Object
ActivityCycle﹕ onPostCreate: bundle=Object
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged


======================================================================
Activity:                            new created
Fragment retainInstance:   false
Kill Activity immediately:   true
ActivityCycle﹕ onCreate: bundle=null
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=null
FragmentCycle===>﹕ onCreateView: bundle=null
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=null
FragmentCycle===>﹕ onActivityCreated: bundle=null
FragmentCycle===>﹕ onViewStateRestored: bundle=null
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onPostCreate: bundle=null
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged


Activity:                            rotate new created
Fragment retainInstance:   false
Kill Activity immediately:   true
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop
ActivityCycle﹕ onDestroy
FragmentCycle===>﹕ onDestroyView
FragmentCycle===>﹕ onDestroy
FragmentCycle===>﹕ onDetach
ActivityCycle﹕ onDetachedFromWindow
ActivityCycle﹕ onCreate: bundle=Object
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=Object
ActivityCycle﹕ onCreateView
          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onCreateView: bundle=Object
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=Object
FragmentCycle===>﹕ onActivityCreated: bundle=Object
FragmentCycle===>﹕ onViewStateRestored: bundle=Object
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onRestoreInstanceState: bundle=Object
ActivityCycle﹕ onPostCreate: bundle=Object
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged

Activity:                            home button pressed
Fragment retainInstance:   false
Kill Activity immediately:   true
ActivityCycle﹕ onPause
FragmentCycle===>﹕ onPause
ActivityCycle﹕ onWindowFocusChanged
ActivityCycle﹕ onSaveInstanceState: outState=Object
FragmentCycle===>﹕ onSaveInstanceState: outState=Object
ActivityCycle﹕ onStop
FragmentCycle===>﹕ onStop
ActivityCycle﹕ onDestroy
FragmentCycle===>﹕ onDestroyView
FragmentCycle===>﹕ onDestroy
FragmentCycle===>﹕ onDetach
ActivityCycle﹕ onDetachedFromWindow

Activity:                            back from background
Fragment retainInstance:   false
Kill Activity immediately:   true
ActivityCycle﹕ onCreate: bundle=Object
FragmentCycle===>﹕ onAttach
ActivityCycle﹕ onAttachFragment
FragmentCycle===>﹕ onCreate: bundle=Object
ActivityCycle﹕ onCreateView          Called many times here
ActivityCycle﹕ onStart
FragmentCycle===>﹕ onCreateView: bundle=Object
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
FragmentCycle===>﹕ onViewCreated: bundle=Object
FragmentCycle===>﹕ onActivityCreated: bundle=Object
FragmentCycle===>﹕ onViewStateRestored: bundle=Object
FragmentCycle===>﹕ onStart
ActivityCycle﹕ onRestoreInstanceState: bundle=Object
ActivityCycle﹕ onPostCreate: bundle=Object
ActivityCycle﹕ onResume
ActivityCycle﹕ onPostResume
ActivityCycle﹕ onResumeFragments
FragmentCycle===>﹕ onResume
ActivityCycle﹕ onAttachedToWindow
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onCreateView
ActivityCycle﹕ onWindowFocusChanged

Friday, August 9, 2013

Add new bank account to Google wallet / Android merchant account - Invalid input error for Bank code or Branch code

Recent when I tried to add new bank account into my payout settings linked to my Google Wallet for my Android apps, I got some wired errors. Here is the error and solution. Please note that I am in Australia. But it should work for other countries with similar

Error:



Solution: 

Please check out this info through the link below. It states how the bank code is composed.

Here is the solution for Australia developers:
In Australia, BSB number has 6 digits. Forget about swift code if you see something like this. So for developers from other countries you should  be able to figure error out with similar tricks.
  • Bank code = FIRST 3 DIGITS OF BSB
  • Branch code = SECOND 3 DIGITS OF BSB



Tuesday, February 12, 2013

Android MVVM Implementation

Just an update for this blog. I recently published an open source project AndroidMVC to help apply MVC/MVVM design pattern for Android Development.
Please check the URLs below

http://kejunxia.github.io/AndroidMvc/

https://github.com/kejunxia/AndroidMvc


Below was the old post
========================================================================
In my lastest a few Android projects, I tried to adopt MVVM pattern. I think the event driven mode is very good for the android app. Here is a Sample Android Project in Github.

I created this MVVM variation for Android app. In this pattern, it doesn't use command wrapper thus there is no reflection which may impact the performance.

Here is the the brief graph illustrating the general idea


Here are the explainations for the objects shown above

Model
Models for holding states and data. Models can be referenced and manipulated by View models.
ViewModel State Model
ViewModels State Model represent the state of views but delegated by ViewModel. It has one to one relationship to ViewModel. ViewModel State Model can be considered as the data of ViewModel which can be bound and updated by ViewModel. As ViewModel State Model only hold the data of ViewModel, it's very easy to serialize into JSON string and re-populated from JSON String.

This is very useful because this make things easy to store the state of views in onSaveInstanceState(Bundle outState) callback in Android activity and fragment. I met the problem that when you leave the app in the background for a long time and when you active the app later, the app crashes. In that case I used static fields to hold state of views. However the Android will clear the static fields when it needs to recollect resources, and then when you are back to the app the static data become null and app crashed. Another difficulty is it's very very tricky to detect if the app if back from the background and ever been killed by the Android OS. So be careful to use static variable to store state. Instead, using a easy serializable java object and save the state in onSaveInstanceState(Bundle outState) and restore them in onCreate, onCreateView or other initializing calls to restore data. If it's too slow, try to use Parcelable.

Another reason to avoid using static field is that, it makes fragment and activity more independent. To use one you just need to give them the initial binding data in a Intent rather than relying on some classes holding the static fields.
ViewModel
ViewModels incorporate with views. Views register callbacks to ViewModels and then when they call method of ViewModels, the ViewModel execute a procedure and then fire the call back to update Views. When a ViewModel bind a data it should update the View referencing it.
As ViewModels can be registered by multiple views, it's very easy to keep all related views up to date all the time.
ViewModelEevnt
Interfaces define viewmodel events. View register these events to the ViewModel which is dependent by the View. When view wants to do something, it calls viewModel.doSomething() and then the view received the event "onSomethingHapped()". 
View
Views incorporate with ViewModels. It can be a concrete view such as a button, a text view, a custom view, a fragment, a activity and etc. Also it can be thought as a abstract concept. For example, the whole application which does have visible UI components can be considered as a view as well. Let's call it AppView. AppView can use a UserViewModel and register LoggedInEvent. And keep the UserViewModel as a app wide variable by a static field or property. Then when other activity/fragment or whatever issued a login call then the AppView will receive logged in event callback and do corresponding updates. And show the UIs for logged users by for example removing current fragments for guest users and load fragments for logged in users. In this case it works a little similar as Controller in MVC pattern.


Code example
Let's try to do a video player. The whole sample project can be find in Github


The Model
This data model hold the info about a video.

public class Video {
 private String mName;
 private int Duration;

 public String getName() {
  return mName;
 }

 public void setName(String name) {
  mName = name;
 }

 public int getDuration() {
  return Duration;
 }

 public void setDuration(int duration) {
  Duration = duration;
 }

}


The ViewModel State Model
The model holds the state the if a video is set for playing and whether or not it's playing.

public class VideoPlayerState {
        // ViewModel State Model contains a data model
 private Video mCurrentVideo;
 private boolean mPlaying;

 public Video getCurrentVideo() {
  return mCurrentVideo;
 }

 public void setCurrentVideo(Video currentVideo) {
  mCurrentVideo = currentVideo;
 }

 public boolean isPlaying() {
  return mPlaying;
 }

 public void setPlaying(boolean playing) {
  mPlaying = playing;
 }

}


A ViewModelEvent
Callbacks when video is played and paused.

public interface VideoPlayerViewModelEvent extends BaseViewModelEvent<VideoPlayerState> {
 void onVideoPlayed(Video video);

 void onVideoPaused(Video video);
}


The ViewModel

The video player view model handles play and pause videos can update the state model accordingly.

public class VideoPlayerViewModel extends
  BaseViewModel<VideoPlayerViewModelEvent, VideoPlayerState> {

 public void playVideo() {
  if (mModel != null) {
   if (!mModel.isPlaying()) {
    mModel.setPlaying(true);
    for (VideoPlayerViewModelEvent evet : this) {
     evet.onVideoPlayed(mModel.getCurrentVideo());
    }
   }
  }
 }

 public void pauseVideo() {
  if (mModel != null) {
   if (mModel.isPlaying()) {
    mModel.setPlaying(false);
    for (VideoPlayerViewModelEvent evet : this) {
     evet.onVideoPaused(mModel.getCurrentVideo());
    }
   }
  }
 }
}


The View
The view is an activity. The part that to save and restore the instance state can be abstracted out into a base class. However, this can't be done very generic as views will derive from different classes such as ViewGroup, Activity, Fragment and whatever else.
public class VideoPlayerView extends Activity implements OnClickListener {
 private static ObjectMapper sMapper;
 private static final String KEY_STATE_JSON = "KeyStateJson";

 private static final String TAG = VideoPlayerView.class.getSimpleName();
 private TextView mTextPlayingVideo;
 private TextView mTextVideoLen;
 private ImageView mPlayingButton;
 private Button mBtnLoadVideo;
 private VideoPlayerViewModel mViewModel;
 private VideoPlayerViewModelEvent mEvent;

 private VideoPlayerState mVideoPlayerState;

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

  mPlayingButton = (ImageView) findViewById(R.id.imgPlayerButton);
  mPlayingButton.setOnClickListener(this);
  mTextPlayingVideo = (TextView) findViewById(R.id.txtPlayingVideoTitle);
  mTextVideoLen = (TextView) findViewById(R.id.txtVideoLength);
  mTextPlayingVideo.setOnClickListener(this);
  mBtnLoadVideo = (Button) findViewById(R.id.btnLoadVideo);
  mBtnLoadVideo.setOnClickListener(this);

  mViewModel = new VideoPlayerViewModel();

  // Setup events handler for the view models
  mEvent = new VideoPlayerViewModelEvent() {
   @Override
   public void onDataBound(VideoPlayerState dataModel) {
    if (dataModel == null) {
     mTextPlayingVideo.setText("No Video");
     mTextVideoLen.setText("0s");
     mPlayingButton.setImageResource(R.drawable.video_play_drawable);
     mBtnLoadVideo.setText("Load Video");
    } else {
     if (dataModel.getCurrentVideo() != null) {
      mTextPlayingVideo.setText(dataModel.getCurrentVideo().getName());
      mTextVideoLen.setText(dataModel.getCurrentVideo().getDuration() + "s");
      mPlayingButton.setImageResource(R.drawable.video_pause_drawable);
     }
     mBtnLoadVideo.setText("Unload Video");
    }

   }

   @Override
   public void onVideoPlayed(Video video) {
    mTextPlayingVideo.setText(video.getName());
    mPlayingButton.setImageResource(R.drawable.video_pause_drawable);
    Toast.makeText(getApplicationContext(), video.getName() + " is played.",
      Toast.LENGTH_SHORT).show();
   }

   @Override
   public void onVideoPaused(Video video) {
    mPlayingButton.setImageResource(R.drawable.video_play_drawable);
    Toast.makeText(getApplicationContext(), video.getName() + " is paused.",
      Toast.LENGTH_SHORT).show();
   }
  };
  // Register events.
  mViewModel.registerEvent(mEvent);

  // Use JSON to serialize state which is easy and quick, if too slow
  // replace it by Parcealable instead.
  if (savedInstanceState == null) {
   // Check if invoking activity send a video to play
   if (getIntent() != null) {
    if (getIntent().hasExtra(KEY_STATE_JSON)) {
     try {
      mVideoPlayerState = getMapper().readValue(
        getIntent().getStringExtra(KEY_STATE_JSON), VideoPlayerState.class);
     } catch (IOException e) {
      Log.e(TAG, e.getMessage(), e);
     }
    }
   }
  } else {
   // Restore the instance state when
   // 1. rotating the phone,
   // 2. back from background when it's killed by OS

   // This can be done in a base class for all view
   if (savedInstanceState.containsKey(KEY_STATE_JSON)) {
    try {
     mVideoPlayerState = getMapper().readValue(
       savedInstanceState.getString(KEY_STATE_JSON), VideoPlayerState.class);
     mViewModel.bindData(mVideoPlayerState);
    } catch (IOException e) {
     Log.e(TAG, e.getMessage(), e);
    }
   }
  }
  // No pre set player state, create a new one for testing.
  if (mVideoPlayerState == null) {
   mVideoPlayerState = new VideoPlayerState();
   Video video = new Video();
   video.setName("Test Video");
   video.setDuration(90);
   mVideoPlayerState.setCurrentVideo(video);
  }
 }

 // Remember to save instance state. Using static is dangerous as Android OS
 // will clear it in background. If you rely on them, you will find they
 // suddenly turned null when app came back from the background after a long
 // time period.

 // This can be done in a base class for all view
 @Override
 protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  if (mViewModel.getModel() != null) {
   try {
    outState.putString(KEY_STATE_JSON,
      getMapper().writeValueAsString(mViewModel.getModel()));
   } catch (IOException e) {
    Log.e(TAG, e.getMessage(), e);
   }
  }
 }

 @Override
 public void onClick(View view) {
  switch (view.getId()) {
   case R.id.btnLoadVideo:
    // Load unload button clicked.
    if (mViewModel.getModel() == null) {
     mViewModel.bindData(mVideoPlayerState);
    } else {
     mViewModel.bindData(null);
    }
    break;
   case R.id.imgPlayerButton:
    // Play/Pause button clicked.
    VideoPlayerState m = mViewModel.getModel();
    if (m != null) {
     if (!m.isPlaying()) {
      mViewModel.playVideo();
     } else {
      mViewModel.pauseVideo();
     }
    } else {
     mViewModel.bindData(mVideoPlayerState);
     mViewModel.playVideo();
    }
    break;
   default:
    break; // do nothing
  }
 }

 private static ObjectMapper getMapper() {
  if (sMapper == null) {
   sMapper = new ObjectMapper();
  }
  return sMapper;
 }
}

Thursday, January 17, 2013

Error "Program bash not found in PATH" when building cocos2dx with eclipse cdt build command

Setting up Cocos2dx Android project is tricky. Though this article is very good as a guidance, I still got some problems. One is
When setting the command build in eclipse CDT with the command
bash ${ProjDirPath}/build_native.sh NDK_DEBUG=1 V=1
it complained
"Program "[path]/bash" not found in PATH".
However, we need to use build_native.sh to build the project because Cocos2dx team has done it well and it also configured paths for cocos2dx project(though you can do your own as long as you want to challenge yourself on this). So we need to find a way to run the build_native.sh.

After blood sweat and tears, I found out that this was because I used the NDK plugin to convert the project into C++ project and NDK plugin set builder Android Builder as below

Then the eclipse will be using Android NDK settings to build to project and could not recognise the bash command.

If you use "add native support" to convert the project to C++ project by Android tools and met the same problem. Switch the current builder to GNU Maker Builder, problem fixed!

Make sure you go back c/c++ Build option to reset the build command line to 
bash ${ProjDirPath}/build_native.sh NDK_DEBUG=1 V=1
as switching current builder will reset build command to default one!

Monday, October 15, 2012

Android R file can't generate

I engaged a wired problem that the R file can't be generated by eclipse even I clean the project a few time. What happened was I broke the project and also the git file so I clone the project from my bitbucket. When I added the git repos and imported the projects, I found the tricky problem.

After googling a while, I hadn't found right solution. After all, I tried my luck by

switching the android SDK version for the project - because for unknown reason the current sdk version is not working for the project.

and it works. I switched it from 4.0.3 to 4.0.0 and right after the eclipse started building the R file. And then I switched it back to 4.0.3 and it seemed back to normal but once I cleaned the project it failed to generate R file again. So I had to stick with 4.0.0. Strange!

Wednesday, November 9, 2011

Android crash report

There is a great library called "ARCA" for collecting android crashes on the fly.

It is open source. What you need to do is just go to the link below and use it for free and it is super easy!

http://code.google.com/p/acra/

Thursday, October 20, 2011

How to build Android application package (.apk) from the command line using the SDK tools + continuously integrated using CruiseControl.

Original article is in here

Hello all android developers, I just want to share my experience building android apk manually using sdk tools without using Eclipse. My original goal is motivated firstly by the desire to incorporate continuous integration aspect to Android development process and secondly to ditch the ADT eclipse plugin since the plugin auto-build process blocks Eclipse UI if you have large resources, assets in your Android project, and a slow computer like mine. I am using CruiseControl as my continuous integration tool.

Below is one of the many apk build processes:

Build process

Build process

The good thing about building manually your apk is that you don’t have to name your resources directory to res, you can name it anything you want.

You can find ant scripts in: \platforms\android-1.5\templates\android-rules.xml

Step 1: Generate Resource java code and packaged Resources
aapt package -f -M ${manifest.file} -F ${packaged.resource.file} -I ${path.to.android-jar.library} -S ${android-resource-directory} [-m -J ${folder.to.output.the.R.java}]

Step 2: Compile java source codes + R.java
use javac

Step 3: Convert classes to Dalvik bytecodes
use dx.bat
dx.bat –dex –output=${output.dex.file} ${compiled.classes.directory} ${jar files..}

Step 4: Create unsigned APK
use apkbuilder

apkbuilder ${output.apk.file} -u -z ${packagedresource.file} -f ${dex.file}

or

apkbuilder ${output.apk.file} -u -z ${packagedresource.file} -f ${dex.file} -rf ${source.dir} -rj ${libraries.dir}

-rf = resources required for compiled source files?
-rj = resources required for jar files

Step 6: Generate a key
use keytool

Step 7: Sign APK
use jarsigner

jarsigner -keystore ${keystore} -storepass ${keystore.password} -keypass ${keypass} -signedjar ${signed.apkfile} ${unsigned.apkfile} ${keyalias}

Step 8: Publish
use adb
adb -d install -r ${signed.apk}

Inspecting your APK file:

aapt list -v latest.apk

Open questions:
1. Can you include more than one dex file in the apk?
2. Can you have dex file named other than classes.dex in the apk?
3. Does an apk have to have a packaged resource?

Note: If upon installing your app using adb you see this error code FAILED_INSTALL_DEXOPT then most likely that either you don’t have classes.dex or you don’t have a packaged resource in the apk


See this post

http://asantoso.wordpress.com/2009/09/15/how-to-build-android-application-package-apk-from-the-command-line-using-the-sdk-tools-continuously-integrated-using-cruisecontrol/

Saturday, October 8, 2011

Threads in android UI programming

Two rules:
  1. Do not block the UI thread
  2. Do not access the Android UI toolkit from outside the UI thread
Therefore, only use UI thread message queue to update UI components.

If task is heavy, do the heavy-lift job on another thread and create a callback on UI(main) thread for the worker thread to call back.

A better way is to use AsyncTask straight away.

Thursday, April 7, 2011

Android Activity Life Cycle on different cases

After published a game on market place, I decided to strengthen my knowledge of Android again.

First, start from Activity Life Cycle. Though the developer site of official Android explained the life cycle of activity with a lot text, it is still a little confusing or not clear enough. Thus I test it through by set break points during the whole life cycle. This is the result.

New Start:
onCreate(),
onStart(),
onResume().

Rotate Device:
onSaveInstanceState(),
onPause(),
onStop(),
onDestroy(),
onCreate(),
onStart(),
onRestoreInstanceState(),
onResume().

Other activity overlayed(e.g.Home button pressed,Calls in or other activity opened):
onSaveInstanceState(),
onPause(),
onStop().

Back from other activity(Not destroyed yet):
onRestart(),
onStart(),
onResume().

Back button pressed/finish() called:
onPause(),
onStop(),
onDestroy().

Screen Off:
onSaveInstanceSave(),
onPause().

Screen back on:
onResume().