I have a very simple preferences setup in which a PreferenceFragment is added to an Activity. The Activity is also an OnSharedPreferenceChangeListener, since I want to update the summary of a particular preference whenever that preference is updated. Here's the Activity:
public class PrefsActivity extends Activity implements OnSharedPreferenceChangeListener {
private static final String PREF_KEY = "key goes here";
private PrefsFragment pf;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pf = new PrefsFragment();
getFragmentManager().beginTransaction().replace(android.R.id.content, pf).commit();
// pf.getPreferenceScreen() throws a NullPointerException here
}
#Override
protected void onPause() {
super.onPause();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
}
#Override
protected void onResume() {
super.onResume();
updateSummary();
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);
}
#Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (key.equals(PREF_KEY)) {
updateSummary();
}
}
private void updateSummary() {
Preference p = pf.getPreferenceScreen().findPreference(PREF_KEY);
p.setSummary("Some string containing the updated value");
}
}
The PreferenceFragment is equally simple:
public class PrefsFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
// At this point getPreferenceScreen() returns correctly
}
}
Here's the thing: Calling getPreferenceScreen() on the PreferenceFragment, immediately after it's been instantiated and added to the Activity in onCreate of PrefsActivity, throws a NullPointerException. It seems that getPreferenceScreen() starts returning a PreferenceScreen object in onCreate() of PrefsFragment, immediately after the call to addPreferencesFromResource() returns.
So my question is this: Since getPreferenceScreen() throws an NPE immediately after the PreferenceFragment has been added to the Activity, is the onCreate() of the PreferenceFragment called asynchronously/in a different thread? Otherwise I would have expected getPreferenceScreen() to return normally immediately after getFragmentManager().beginTransaction().replace(android.R.id.content, pf).commit().
is the onCreate() of the PreferenceFragment called asynchronously/in a
different thread?
No, it is run synchronously on the main thread. That means it has to wait until the Activitys onCreate exits before it gets to run. The FragmentManager schedules the Fragment callbacks, but they are not executed until the current callback (onCreate of the activity) is complete.
You have a few other places to access your PreferenceScreen. OnStart is called when the UI is ready, so the Fragment will be ready. onResume is called after onStart and is probably the best place to put something since it is invoked after coming back from a pause, too.
Related
Edit: Changing the language results in SettingsActivity.onCreate being called twice: (1) Due to recreate() and (2) due to context change.
However SettingsFragment.onCreate is being called only once, which kind of explains below question. But why isnt't SettingsFragment.onCreate being called after the second execution of the SettingsActivity.onCreate?
I am quite new to Android development and want to understand the reason which makes my application crash:
Within a settings activity I have placed a PreferenceFragment which allows the user to change the UI language. In order to take effect and not to spoil the back stack I call the recreate() method of the fragment's activity.
The first implementation looked like below:
(A)
public class SettingsActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SettingsFragment fragment = new SettingsFragment();
getFragmentManager().beginTransaction().replace(android.R.id.content, fragment).commit();
getFragmentManager().executePendingTransactions();
fragment.setActivity(this);
}
public static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener
{
Activity m_activity;
public void setActivity(Activity activity){
m_activity = activity;
}
#Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
SharedPreferences preferences = getPreferenceScreen().getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(this);
}
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
...
m_activity.recreate();
}
}
}
Sometimes above solution works, sometimes m_activity is Null and the application crahes. As a possible alternative solution I removed the 'setActivity' setter and called 'getActivity' within onSharedPreferenceChanged:
(B)
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
...
getActivity().recreate();
}
But this also results in sporadic NullPointerExceptions.
In scenario (B) I found that the fragment's 'onAttach' is not being called for any reason sometimes and hence 'getActivity' returns null. However I would expect scenario (A) not to crash since this isn't Null in onCreate?
Problem solved!
For those who have the same problem: I had to unregister the onSharedPreferenceChanged listener in SettingsFragment.onDestroy.
I have an AppCompatPreference SettingsActivity with a PreferenceFragment, like this:
public class SettingsActivity extends AppCompatPreferenceActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "activity onCreate called");
setupActionBar();
String userString = getIntent().getStringExtra(LoginActivity.USER);
Log.v(TAG, "UserString: " + userString);
...
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class GeneralPreferenceFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "GeneralPreferenceFragment onCreate called");
addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true);
}
}
}
When I start the app, a LoginActivity authenticates with a server and passes user data (userString) to the SettingsActivity. It then starts a service with that data.
Everything is peachy and the service starts with no problem.
D/SettingsActivity: activity onCreate called
V/SettingsActivity: UserString: {some string of JSON user data}
But then I tap on General Preferences. As soon as I do so, this gets logged:
D/SettingsActivity: activity onCreate called
V/SettingsActivity: UserString: null
Because it logs activity onCreate called instead of GeneralPreferenceFragment onCreate called, it seems like the wrong onCreate() is being called. The app then crashes with a NullPointException trying to start the service with a null user.
I am trying to figure this out. Maybe the entire activity is restarting for some reason? Any suggestions on diagnosing this problem would help.
As your log shows, a new instance of activity is created.
This is the expected behaviour of the PreferenceActivity on a phone. Tablets use a two-pane layout and keep a single activity. But phones start a new activity.
AppCompat behaves the same.
You can however pass more data to the fragment with
public class MySettingsActivity extends PreferenceActivity {
#Override
public void onBuildHeaders(List<Header> target) {
super.onBuildHeaders(target);
// You can build with xml settings that don't depend from UserString
loadHeadersFromResource(R.xml.preferences, target);
// For Settings that depend on UserString:
Header userHeader = new Header();
userHeader.title = ""; // TODO
user.fragment = UserFragment.class;
Bundle args = new Bundle(1);
// TODO Pass a User parcelable instead
args.putString(EXTRA_USER, userString);
userHeader.fragmentArguments = args;
}
}
There are a lot of answered questions in Stackoverflow about checking if activity is null from a fragment, using getActivity()==null
How do I check if activity is not null in the activity itself?
My specific case is this:
activity starts an asynctask, then activity is destroyed, then asynctask returns in onPostExecute, it invokes a method in the activity (which is registered as a listener for that task) and this method uses a reference to THIS to pass a context in a method. The context is null, though.
EDIT: Here is some code.
public interface OnGetStuffFromServerListener {
void onGetStuffSuccess();
}
public class SomeActivity implements OnGetStuffFromServerListener {
#Override
public whatever onCreate() {
new GetStuffFromServer(this).execute();
}
#Override
public void onGetStuffFromServerSuccess() {
deleteSomeFiles(this); // NPE -> How do I check if activity still exists here?
}
private void deleteSomeFiles(Context context) {
...
context.getExternalFilesDir(null).toString(); // NPE toString on a null object reference
}
}
public class GetSomeStuffFromServer extends AsyncTask<Void, Void, Void> {
private OnGetSomeStuffFromServerListener listener;
public GetSomeStuffFromServer (OnGetSomeStuffFromServerListener listener) {
this.listener = listener;
}
...doInBackground
onPostExecute() {
if(listener!=null) {
listener.onGetSomeStuffFromServerSuccess();
}
}
}
Actually, if I am using getApplicationContext() instead of this, maybe I will not have a problem at all?
I'm not sure why your Activity is being destroyed. Although you may be able to recreate the Activity using a Bundle. Google's documentation on Activities gives the following sample for saving and restoring an instance of your activity.
The following will save the state of your Activity:
static final String STATE_SCORE = "playerScore";
static final String STATE_LEVEL = "playerLevel";
...
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current game state
savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
The following will be called to restore your Activity's previous state. Note that the logic is contained in onCreate(), so it sounds like you will have initialize your Activity again.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // Always call the superclass first
// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
// Restore value of members from saved state
mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
} else {
// Probably initialize members with default values for a new instance
}
...
}
Let me know if that helps!
edit:
Try canceling the operation in onDestroy(). If the Activity has called onDestroy() its memory has been released by the device. Make sure you aren't disposing of your Activity anywhere else in your code.
#Override
protected void onDestroy() {
asynctask.cancel(true);
super.onDestroy();
}
Use myActivity.isDestroyed()
reference to doc
I just recently started learning how to build android apps, and encountered a problem:
I want, when users leave the app (go to the homescreen, multitask), and they return, that the app calls a certain method. How can I do that?
This problem is more tricky than it may look like. When you return to app after leaving it, then is called method onResume of activity which was active when app was interrupted. But same happens when you go from one activity to another (onResume of second activity is called). If you just call method from onResume, it will be called every time onResume of any activity is called.
Take a look at this solution...
First, you have BaseActivity which is extended by all activities that need to call that method:
abstract public class BaseActivity extends Activity implements IName {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
protected void onResume() {
if (AppClass.getPausedActivity() != null) {
if (this.getClassName().equals(AppClass.getPausedActivity()))
//call specific method
}
AppClass.setPausedActivity("");
super.onResume();
}
#Override
protected void onPause() {
AppClass.setPausedActivity(this.getClassName());
super.onPause();
}
#Override
abstract public String getClassName();
}
As you can see it implements interface IName:
public interface IName
{
String getClassName();
}
BaseActivity in onPause (when it is interrupted) calls setPausedActivity method of AppClass which remembers last activity name that was interrupted. In onResume (when app and activity is continued) we compare name of current activity and last paused activity.
So, when app is interrupted, these names will be same because you paused one activity and you got back to the same one. When you call activity from some other activity these names will not be same and method will not be called.
Here is code for AppClass:
public class AppClass extends Application {
public static String pausedActivity;
public static String getPausedActivity() {
return pausedActivity;
}
public static void setPausedActivity(String _pausedActivity) {
pausedActivity = _pausedActivity;
}
}
Also, here is example of activity that extends BaseActivity:
public class MainActivity extends BaseActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
//here you set name of current activity
#Override
public String getClassName() {
return "MainActivity";
}
}
You are bound to the Activity lifecycle. You will need to implement corresponding logic to figure out if the user has been in your app before (i.e. using SharedPreferences).
I have an Activity with a private OnSharedPreferenceChangeListener, the listener's work is defined on the onCreate() method of the Activity. The listener is registered to the sharedPreferences of the application.
The change itself is triggered by a Service in response to an sms received intent.
Will the listener receive the callback when the Activity itself has died? are there cases where it will not?
The listener is defined (roughly):
private OnSharedPreferenceChangeListener _sharedPreferenceListener;
public void onCreate(Bundle bundle){
...
_prefs = PreferenceManager.getDefaultSharedPreferences(this);
_prefs.registerOnSharedPreferenceChangeListener(_sharedPreferenceListener);
...
_sharedPreferenceListener = new SharedPreferences.OnSharedPreferenceChangeListener(){ /*doing some work here*/};
...
}
please igonre the logic here if correct or not, assume that the code works, my main concern is how the listener reacts to changes in the lifecycle of the activity.
Thanks,
actually, since the listener doesn't know anything about the activity (and as such you can use it anywhere , not just in an activity), you will get notified no matter where you use it.
Also, since you can't know for sure what it does with the context , you should use the application context instead in this case (so that you won't have memory leaks, though I doubt it needs a reference to the activity).
Of course, if the listener itself is referenced by weak reference, and the activity doesn't have any reference to itself on any other class, the listener can be GC-ed too. You can see in the code of Android (or at least of API 19) that in the class "android.app.SharedPreferencesImpl" (example link here) , you have a WeakHashMap of listeners, so it might mean that the activity that hosts the listener can be GC-ed and so the listener will stop from being called. Here is the relavant code of Android:
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
...
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.put(listener, mContent);
}
}
So, as I've written, best if you just put the application context in case you wish to keep listening to this event.
Or, in case you do wish to stop listening to this event, just unregister it when the activity is being destroyed.
to prove it, you can simply run your app...
here's my proof app:
MainActivity.java
public class MainActivity extends Activity {
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
preferences.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
#Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
android.util.Log.d("AppLog", "changed!");
}
});
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
startActivity(new Intent(MainActivity.this, Activity2.class));
}
}, 1000);
finish();
}
}
Activity2.java
public class Activity2 extends Activity {
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_activity2);
//if you call here System.gc(); , you have a good chance that the listener won't be called
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
//this may or may not cause the listener to write to the log
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(Activity2.this);
preferences.edit().putBoolean("test", true).commit();
}
}, 1000);
}
}
Will the listener receive the callback when the Activity itself has died?
-> No, it won't. Because when your activity dies, the _prefs and _sharedPreferenceListener fields will be destroyed.
You could check this question for more details on OnSharedPreferenceChangeListener :
SharedPreferences.onSharedPreferenceChangeListener not being called consistently
You must un-register the listener in onDestroy() of activity, else Activity object will stay in memory.