This document can be read in Google Docs (http://docs.google.com/View?id=ddwc44gs_194s36s83fz), cut and paste link if you have problems accessing it. |
A couple of days ago I've found an interesting post by skink on Android Developers group titled **never ever** use Toasts with Activity context. The post speaking about NotifyWithText in ApiDemos, states something like:
"...try to show any Toast, then exit NotifyWithText
demo. run it again - you will see getInstanceCount() increases leaking
Activities. repeat running demo couple of times. counter still
increases."
So the questions are:
Is Toast leaking Context objects ?
Is it a bug in Toast, in ApiDemos, in the documentation ?
Should we use Application Context instead ?
Let's try to find the answers using some Unit Tests as we have been investigating in previous articles in this blog.
ToastActivity
Let's recreate a oversimplified version of the Activity to display just the Toast depending on an extra parameter DISPLAY_TOAST in the Intent starting the Activity.
package com.codtech.android.training.toast;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
public class ToastActivity extends Activity {
private static final String TAG = "ToastActivity";
public static final String USE_ACTIVITY_CONTEXT =
"com.codtech.android.training.toast.useActivityContext";
public static final String DISPLAY_TOAST =
"com.codtech.android.training.toast.displayToast";
private Context toastContext;
private boolean useActivityContext;
private boolean displayToast;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
useActivityContext =
intent.getBooleanExtra(USE_ACTIVITY_CONTEXT, true);
displayToast =
intent.getBooleanExtra(DISPLAY_TOAST, true);
if ( useActivityContext ) {
toastContext = this;
}
else if ( displayToast ) {
toastContext =
getApplication().getApplicationContext();
}
}
/* (non-Javadoc)
* @see android.app.Activity#onPause()
*/
@Override
protected void onResume() {
super.onResume();
if ( displayToast ) {
Toast.makeText(toastContext,
"Sample Toast: " + getInstanceCount(),
Toast.LENGTH_SHORT).show();
}
else {
Log.d(TAG, "No toast displayed");
}
finish();
}
/* (non-Javadoc)
* @see android.app.Activity#onDestroy()
*/
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "Activity destroyed: " + this);
}
}
ToastActivity Tests
As usual, let's create our test to.
In this very particular case we want it to run several times specified by ToastActivityTests.N, 50 actually but you can change it if you like, to see if displaying a Toast or not change things in some way.
RepeatedTestSuite
This class implements repeated tests.
package com.codtech.android.training.toast.tests;
import junit.framework.Test;
import junit.framework.TestSuite;
public class ReapeatedTestSuite extends TestSuite {
public static Test repeatedSuite(Test test, int count) {
TestSuite suite = new TestSuite("Repeated");
// there's no RepeatedTest in android's junit
for (int i=0; i<count; i++) {
suite.addTest(test);
}
return suite;
}
}
RepeatedActivityContextWithToast
This test will run testSingleActivityContextWithToast starting the activity displaying the Toast several times
/**
*
*/
package com.codtech.android.training.toast.tests;
import junit.framework.Test;
/**
* @author diego
*
*/
public class RepeatedActivityContextWithToast extends ReapeatedTestSuite {
public static Test suite() {
return repeatedSuite(
new ToastActivityTests("testSingleActivityContextWithToast"),
ToastActivityTests.N);
}
}
RepeatedActivityContextWithoutToast
This test will run testSingleActivityContextWithoutToast starting the activity, without displaying any Toast, several times
/**
*
*/
package com.codtech.android.training.toast.tests;
import junit.framework.Test;
/**
* @author diego
*
*/
public class RepeatedActivityContextWithoutToast extends ReapeatedTestSuite {
public static Test suite() {
return repeatedSuite(
new ToastActivityTests("testSingleActivityContextWithoutToast"),
ToastActivityTests.N);
}
}
ToastActivityTests
We decided to fail the test if the Activity instance count reaches N/4.
/**
*
*/
package com.codtech.android.training.toast.tests;
import java.lang.reflect.Method;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.test.ActivityInstrumentationTestCase2;
import android.test.FlakyTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import com.codtech.android.training.toast.ToastActivity;
/**
* @author diego
*
*/
public class ToastActivityTests
extends ActivityInstrumentationTestCase2<ToastActivity> {
private static final String TAG = "ToastActivityTests";
public static final int N = 50;
/**
* @param name
*/
public ToastActivityTests(String name) {
super("com.codtech.android.training.toast",
ToastActivity.class);
setName(name);
}
/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
}
/* (non-Javadoc)
* @see android.test.InstrumentationTestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
}
@MediumTest
public void testSingleActivityContextWithToast() {
exersiseActivityLifecycle(intentFactory(true, true),
"testSingleActivityContextWithToast");
}
@MediumTest
public void testSingleApplicationContextWithToast() {
exersiseActivityLifecycle(intentFactory(false, true),
"testSingleApplicationContextWithToast");
}
@MediumTest
public void testSingleActivityContextWithoutToast() {
exersiseActivityLifecycle(intentFactory(true, false),
"testSingleActivityContextWithoutToast");
}
@MediumTest
public void testSingleApplicationContextWithoutToast() {
exersiseActivityLifecycle(intentFactory(false, false),
"testSingleApplicationContextWithoutToast");
}
/**
* @param intent
*/
private void exersiseActivityLifecycle(final Intent intent, final String name) {
setActivityIntent(intent);
final ToastActivity activity = getActivity();
final Instrumentation instrumentation = getInstrumentation();
// At this point, onCreate() has been called, but nothing else
// Complete the startup of the activity
instrumentation.callActivityOnStart(activity);
instrumentation.callActivityOnResume(activity);
// At this point you could test for various configuration aspects, or you could
// use a Mock Context to confirm that your activity has made certain calls to the system
// and set itself up properly.
instrumentation.callActivityOnPause(activity);
// At this point you could confirm that the activity has paused properly, as if it is
// no longer the topmost activity on screen.
instrumentation.callActivityOnStop(activity);
Runtime.getRuntime().gc();
Runtime.getRuntime().runFinalization();
Runtime.getRuntime().gc();
long aic = ToastActivity.getInstanceCount();
assertTrue("instance count reached " + aic, aic < N/4);
// At this point we are invoking onDestroy explicitly because we are iterating
// and tearDown() will not be called
instrumentation.callActivityOnDestroy(activity);
// run N times, requires FlakyTest
try {
Method method = this.getClass().getMethod(name, new Class[] {});
FlakyTest flakyTest = method.getAnnotation(FlakyTest.class);
if ( flakyTest != null ) {
assertTrue(count >= flakyTest.tolerance());
}
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static Intent intentFactory(boolean useActivityContext,
boolean displayToast) {
final Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.setClassName("com.codtect.android.training.toast",
"com.codtect.android.training.toast.ToastActivity");
intent.putExtra(ToastActivity.USE_ACTIVITY_CONTEXT, useActivityContext);
intent.putExtra(ToastActivity.DISPLAY_TOAST, displayToast);
return intent;
}
}
Running the tests
Running RepeatedActivityContextWithoutToast
Running the tests we can verify that everything is fine.
diego@bruce:~$ adb shell am instrument -w -e class com.codtech.android.training.toast.tests.RepeatedActivityContextWithoutToast com.codtech.android.training.toast.tests/android.test.InstrumentationTestRunner
com.codtech.android.training.toast.tests.ToastActivityTests:...................
...............................
Test results for InstrumentationTestRunner=.........................................
.........
Time: 42.473
OK (50 tests)
Running RepeatedActivityContextWithToast
Running the test that displays the Toast we find a different result. Activity instance count reaches the maximum allowed and the test fails
diego@bruce:~$ adb shell am instrument -w -e class com.codtech.android.training.toast.tests.RepeatedActivityContextWithToast com.codtech.android.training.toast.tests/android.test.InstrumentationTestRunner
com.codtech.android.training.toast.tests.ToastActivityTests:...........
Failure in testSingleActivityContextWithToast:
junit.framework.AssertionFailedError: instance count reached 12
at com.codtech.android.training.toast.tests.ToastActivityTests.
exersiseActivityLifecycle(ToastActivityTests.java:195)
at com.codtech.android.training.toast.tests.ToastActivityTests.
testSingleActivityContextWithToast(ToastActivityTests.java:138)
...
Test results for InstrumentationTestRunner=............F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.
F.F.F.F.F.F.F.F.F.F
Time: 56.101
FAILURES!!!
Tests run: 50, Failures: 39, Errors: 0
Conclusion
We can see that the only difference between both tests is the Toast being displayed, and the results are completely different.
While the demonstration given in the Android Developer's thread seems correct, this seems correct too !
Comments, suggestions and corrections are gladly welcome.
If you are interested in the source code or APK just drop me a line or leave a comment in the blog.
Copyright © 2009 Diego Torres Milano. All rights reserved.