I have a fragment that's used for editing a project. I load all the data with a loader and then let the user edit it. The problem is that when a user enters some data in EditText and then rotates the device the loader reloads the data and overrides all changes made by the user. Of course when I comment out initLoader() the EditText values are retained after rotation.
What are some common patterns stopping reloading of loaders after orientation change? The easiest solution I can come up with is putting some sort of a flag variable into onSaveInstanceState() and adding an if statement in onLoadFinished() to not reload the data, but I'm wondering if there is a better solution. Below is simplified code from my fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_add_edit_project, container, false);
this.projectAddressInput = (EditText) view.findViewById(R.id.fragment_add_edit_project_address);
return view;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
#Override
public Loader onCreateLoader(int id, Bundle args) {
return new CursorLoader(
getActivity(),
Project.buildProjectUri(this.projectId),
PROJECTION,
null,
null,
null
);
}
#Override
public void onLoadFinished(Loader loader, Object data) {
Cursor cursor = (Cursor) data;
if (cursor != null && cursor.moveToFirst()) {
this.projectAddressInput.setText(cursor.getString(cursor.getColumnIndex(Project.COLUMN_ADDRESS)));
}
}
#Override
public void onLoaderReset(Loader loader) {}
Any suggestions much appreciated.
Declare android:configChanges in your AndroidManifest.xml to instruct Activity Manager not to restart your activity on configuration changed (which as a result will reload your CursorLoader):
<activity
...
android:configChanges="orientation|screenSize" />
If your application doesn't need to update resources during a specific configuration change and you have a performance limitation that requires you to avoid the activity restart, then you can declare that your activity handles the configuration change itself, which prevents the system from restarting your activity.
Reference: http://developer.android.com/guide/topics/resources/runtime-changes.html
Related
I'm fetching data in my activity that is needed by several fragments. After the data is returned, I create the fragments. I was doing this via an AsyncTask, but it led to occasional crashes if the data returned after a screen rotation or the app is backgrounded.
I read up and thought the solution to this was instead using an AsyncTaskLoader. Supposedly it won't callback if your activity's gone, so those errors should be solved. But this now crashes every time because "Can not perform this action (add fragment) inside of onLoadFinished".
How am I supposed to handle this? I don't want my fragments to each have to fetch the data, so it seems like the activity is the right place to put the code.
Thanks!
Edit 1
Here's the relevant code. I don't think the problem is with the code per-se, but more of my whole approach. The exception is pretty clear I shouldn't be creating fragments when I am. I'm just not sure how to do this otherwise.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(BREWERY_LOADER, null, this).forceLoad();
}
//================================================================================
// Loader handlers
//================================================================================
#Override
public Loader<Brewery> onCreateLoader(int id, Bundle args) {
int breweryId = getIntent().getIntExtra(EXTRA_BREWERY_ID, -1);
return new BreweryLoader(this, breweryId);
}
#Override
public void onLoadFinished(Loader<Brewery> loader, Brewery data) {
if (data != null) {
onBreweryReceived(data);
} else {
onBreweryError();
}
}
#Override
public void onLoaderReset(Loader<Brewery> loader) {
}
...
protected void onBreweryReceived(Brewery brewery) {
...
createFragments();
}
...
protected void createFragments() {
FragmentManager fm = getSupportFragmentManager();
//beers fragment
mBeersFragment = (BreweryBeersFragment)fm.findFragmentById(R.id.beersFragmentContainer);
if (mBeersFragment == null) {
mBeersFragment = new BreweryBeersFragment();
fm.beginTransaction()
.add(R.id.beersFragmentContainer, mBeersFragment)
.commit();
Bundle beersBundle = new Bundle();
beersBundle.putInt(BreweryBeersFragment.EXTRA_BREWERY_ID, mBrewery.getId());
mBeersFragment.setArguments(beersBundle);
}
}
Edit 2
My new strategy is to use an IntentService with a ResultReceiver. I null out callbacks in onPause so there's no danger of my activity being hit when it shouldn't be. This feels a lot more heavy-handed than necessary, but AsyncTask and AsyncTaskLoader neither seemed to have everything I needed. Creating fragments in those callback methods doesn't seem to bother Android either.
From the MVC (Model -- View -- Controller) viewpoint, both the Activity and its fragments are Controller, while it is Model that should be responsible for loading data. As to the View, it is defined by the layout xml, you can define custom View classes, but usually you don't.
So create a Model class. Model is responsible for what must survive a screen turn. (Likely, it will be a static singleton; note that Android can kill and re-create the process, so the singleton may get set to null.) Note that Activities use Bundles to send data to themselves in the future.
I have a fairly simple DialogFragment. It looks something like:
import android.support.v4.app.DialogFragment;
public class MyDialogFragment extends DialogFragment {
private String mData = "empty";
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(LOG_TAG, "onCreate");
setStyle(DialogFragment.STYLE_NO_TITLE, 0);
// setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.mydialog, container);
....
return view;
}
public setData(String _data) {
mData = _data;
}
}
I load this fragment like so from my FragmentActivity:
FragmentManager lFM = getSupportFragmentManager();
MyDialogFragment lDialog = new MyDialogFragment();
lDialog.setData("not empty");
lDialog.show(lFM, "MyDialog");
The code as above works fine. However I would like to retain the fragment on an orientation switch so that the mData field is preserved. If I add setRetainInstance(true); (and after sticking in some debug) I can see that the fragment is indeed retained on an orientation switch - onCreate() is not being called this time. I can see onCreateView() is being called and I return a correct View object, but the dialog is not shown on the screen. What am I missing?
After reading the answer that baboo gave me I implemented the solution as follows .. I hope this is correct (at least it works ok ...)
#Override
public void onCreate(Bundle savedInstanceState) {
// ....
FragmentManager lFM = getSupportFragmentManager();
if(lFM.findFragmentByTag("MyDialog")!=null)
((MyDialogFragment)lFM.findFragmentByTag("MyDialog")).show(lFM, "MyDialog");
// ....
}
Try the following logic in your fragment activity:
Use the put methods to store values in onSaveInstanceState():
protected void onSaveInstanceState(Bundle icicle) {
super.onSaveInstanceState(icicle);
icicle.putBoolean("dialogDisplayed", value); // set value = true when displayin dialog...
}
And restore the values in onCreate():
public void onCreate(Bundle icicle) {
if (icicle != null){
value = icicle.getBoolean("dialogDisplayed");
}
if(value)
//Display Dialog here....
}
The dialog fragment should be preserved automatically as long as you do the following:
If you call an Activity onSaveInstanceState(), make sure you call the super function!!!!. In my case, that was the key. Also make sure you do the same thing in the Fragment.
If you use setRetainInstance, you will need to manually store off the values and re-apply them. Otherwise, you should be able to not worry about it, in most cases. If you're doing something a bit more complicated, you might need to setRetainInstance(true), but otherwise ignore it. In my case, I needed to use it to store a random seed for one of my classes, but otherwise I was okay.
Some people have complained about a bug in the support library, where a dismiss message is sent when it shouldn't be. The latest support library seems to have fixed that, so you shouldn't need to worry about that.
You shouldn't need to do anything fancy like manually store off the fragment, it should be done automatically if you follow these steps.
In the Android documentation for Loaders found at http://developer.android.com/guide/components/loaders.html it says one of the properties of loaders is that:
They automatically reconnect to the last loader's cursor when being recreated after a configuration change. Thus, they don't need to re-query their data.
The following code does not seem to mirror that behaviour, a new Loader is created an finishes querying the ContentResolver, then I rotate the screen and the Loader is re-created!
public class ReportFragment extends Fragment implements LoaderCallbacks<Cursor> {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(1, null, this);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_report, container, false);
return v;
}
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
Log.d("TEST", "Creating loader");
return new CursorLoader(getActivity(), ResourcesContract.Reports.CONTENT_URI, null, null, null, null);
}
public void onLoadFinished(Loader<Cursor> arg0, Cursor arg1) {
Log.d("TEST", "Load finished");
}
public void onLoaderReset(Loader<Cursor> arg0) {
}
}
Here is the output from my logcat:
08-17 16:49:54.474: D/TEST(1833): Creating loader
08-17 16:49:55.074: D/TEST(1833): Load finished
*Here I rotate the screen*
08-17 16:50:38.115: D/TEST(1833): Creating loader
08-17 16:50:38.353: D/TEST(1833): Load finished
Any idea what I'm doing wrong here?
EDIT:
I should note that I'm building to Android Google API's version 8, and using the v4 support library.
2nd EDIT:
This is most likely due to a bug in the support library, take a look at this bug submission if you want further information:
http://code.google.com/p/android/issues/detail?id=20791&can=5&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars
Though this is an old question, I've been experiencing the same issue as the OP. Using a loader, I need to have it restarting when navigating to a new Activity, and then back. But at the same time, I don't want the loader to restart when I rotate the phone's screen.
What I found is that it is possible to achieve this in onRestart(), if you restart the loader BEFORE calling its super.
public class MainActivity extends AppCompatActivity implements
LoaderManager.LoaderCallbacks<Cursor> {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
//Initialize the loader.
getSupportLoaderManager().initLoader(0, null, this);
}
#Override
protected void onRestart() {
//Restart the loader before calling the super.
getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
super.onRestart();
}
...
}
In my opinion you misunderstood what the documentation says. The documentations says, that they don't need to re-query their data, and it is not doing so.
Try to log/insert a breakpoint in your ContentProvider#query() method! The query will be called only on Activity startup, and not after orientation change.
But this is not true for the LoaderCallbacks#onCreateLoader() method. It will be called after every orientation change, but this not means re-querying, it just calls the method so you can change the CursorLoader if you want.
So far I found that retaining fragment Fragment.setRetainInstance(true) prevents recreating loader on orientation change using support library. The loader last results are nicely delivered in onLoadFinished(). It works at least when activity manages single fragment and the fragment is added to activity using FragmentTransaction.
Though this is a bit old question I would like to put my views here.
There is no need for storing additional info in onSaveInstanceState
The framework automatically reconnect to the last loader's cursor when being recreated after a configuration change. Thus, they don't need to re-query their data.
This means in the onCreate function you need to call loaderManager only if the savedInstanceState is null
Ex:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState == null) {
getLoaderManager().initLoader(1, null, this);
}
}
You can simply check to see if the loader already exists onCreate. Then you can either init or restart.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getLoaderManager().getLoader(LOADER_ID) == null) {
getLoaderManager().initLoader(LOADER_ID, null, this);
} else {
getLoaderManager().restartLoader(LOADER_ID);
}
}
You normally pass an ID to your loader so you can reference it later via the loader manager.
Hope this helps!
onCreate() gets called during screen orientation change since the activity gets destroyed and recreated.
Unless you're loading a lot of data then it doesn't hurt to do, but you can try the following if you want (I haven't tested it, but in theory I think it would work).
Declare a static boolean at the top global of the class. I think you'll also need a static cursor to reference
private static boolean dataDownloaded = false;
private static Cursor oldCursor;
Then on onLoadFinished set dataDownloaded = true
Override onSaveInstanceState to save the boolean value
#Override
protected void onSaveInstanceState(Bundle outSave) {
outSave.putBoolen("datadownloaded", dataDownloaded);
oldCursor = adapter.swapCursor(null);
}
and onCreate add the following
if (savedInstanceState != null) {
this.dataDownloaded = savedInstanceState.getBoolean("datadownloaded", false);
}
adjust your onCreateLoader
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader cursorLoader;
if (dataDownloaded) {
cursorLoader = new CursorLoader(getActivity(),
null, projection, null, null, null);
cursorLoader.deliverResult(oldCursor);
} else {
CursorLoader cursorLoader = new CursorLoader(getActivity(),
URI_PATH, projection, null, null, null);
}
return cursorLoader;
}
I'd like to use a demo to show this:
enter code here
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button button = (Button) findViewById(R.id.button1);
button.setOnClickListener(buttonClickListener);
}
private OnClickListener buttonClickListener = new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
startMyLoader();
}
};
private void startMyLoader() {
getLoaderManager().destroyLoader(0);
getLoaderManager().restartLoader(0, null, myLoaderListener);
}
/**
* The listener for the group metadata loader.
*/
private final LoaderManager.LoaderCallbacks<Cursor> myLoaderListener
= new LoaderCallbacks<Cursor>() {
#Override
public CursorLoader onCreateLoader(int id, Bundle args) {
return new CursorLoader(LoaderDemoActivity.this,
ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursor.moveToPosition(-1);
if (cursor.moveToNext()) {
Context context = getApplicationContext();
CharSequence text = "Load finished!";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
};
enter code here
After orientation changed, I clicked the button,
the onCreateLoader can be called,
but onLoadFinished will not be called.
It seems strange.
Thanks for help in advance.
I faced the same problem. Please make a try call this.getSupportLoaderManager() in onCreate.
It solved my problem. Hope it will help you as well
I think I have found the reason.
In Activity onCreate, it will load all the LoaderMangers(of its own or its sub-Fragments)
from NonConfigurationInstances.
if (mLastNonConfigurationInstances != null) {
mAllLoaderManagers = mLastNonConfigurationInstances.loaders;
}
And in Activity onStart, it will try to start its own LoaderManger.
if (!mLoadersStarted) {
mLoadersStarted = true;
if (mLoaderManager != null) {
mLoaderManager.doStart();
} else if (!mCheckedForLoaderManager) {
mLoaderManager = getLoaderManager(-1, mLoadersStarted, false);
}
mCheckedForLoaderManager = true;
}
But after config changed, mLoaderManager == null, so it will not start it.
And here is the problem!
If you try to start loader belong to this loaderManager, it will fail.
void installLoader(LoaderInfo info) {
mLoaders.put(info.mId, info);
if (mStarted) {
// The activity will start all existing loaders in it's onStart(),
// so only start them here if we're past that point of the activitiy's
// life cycle
info.start();
}
}
note the mStarted value which will be set 'true' when LoaderManager started.
And there is two ways to solve this problem.
call getLoaderManger() in onCreate(), it will re-assign the mLoaderManager
and make it ready to be started in the subseuqent onStart().
public LoaderManager getLoaderManager() {
if (mLoaderManager != null) {
return mLoaderManager;
}
mCheckedForLoaderManager = true;
mLoaderManager = getLoaderManager(-1, mLoadersStarted, true);
return mLoaderManager;
}
have the loader located in fragments. Because in Fragments' onStart(),
it will start its own LoaderManager.
if (!mLoadersStarted) {
mLoadersStarted = true;
if (!mCheckedForLoaderManager) {
mCheckedForLoaderManager = true;
mLoaderManager = mActivity.getLoaderManager(mIndex, mLoadersStarted, false);
}
if (mLoaderManager != null) {
mLoaderManager.doStart();
}
}
You don't need to (neither ought to) destroy your Loader in order to reload it. Loader class is intended to be reusable.
Use initLoader instead. eg.:
getLoaderManager().initLoader(0, null, myLoaderListener);
If you want to force reloading allready registered loader:
getLoaderManager().getLoader(0).forceLoad();
If you are not sure if the Loader instance allready exists after configuration change event happened use initLoader instead of getLoader to retrieve your Loader instance on which you can call forceLoad().
getLoaderManager().initLoader(0, null, myLoaderListener).forceLoad();
If you use support library then use forceLoad even after first instantation - there is probably a bug - I remind myself there are some questions about it on this forum - try searching older posts.
Make sure you are not checking savedStateInfo while using fragments before you call your loader in activity onCreate
#Override
public void onCreate(Bundle savedInstanceState) {
// used to not overlap fragments
if (savedInstanceState != null) {
return null;
}
loadFragments();
getSupportLoaderManager().restartLoader(LISTS_LOADER, null, this);
}
If you need to check for savedInstanceState fragments anyway you can check for any class variable that should be created after loader finished loading, as activity gets destroyed when rotating, but raising from previous state when rotating back.
From the android's development site
"They automatically reconnect to the last loader's cursor when being
recreated after a configuration change. Thus, they don't need to
re-query their data."
As far as I understand even when we start the loader explicitly the loader won't start. Since the destroy which we are calling should actually call onLoaderReset() once it is destroyed. But that method is not called once the orientation is changed, but is called before.
Still I may be wrong in this. This is my assumption. Further discussion would be appreciated.
I have a View that was created on runtime then I draw some canvas on that View(runtime) after that I rotated my screen.All data was gone(reset).So I put the some code in AndroidManifest.xml like this
android:configChanges="keyboardHidden|orientation"
in my <activity> then I put a #Override function
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setContentView(R.layout.main);
LinearLayout layout = (LinearLayout) findViewById(R.id.myPaint);
layout.addView(mView);
}
but everything couldn't solved my problem.I want to keep my data from View(runtime) on every single rotation.
That's my onCreate function.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mView = new MyView(this);
setContentView(mView);
mView.requestFocus();
setContentView(R.layout.main);
LinearLayout layout = (LinearLayout) findViewById(R.id.myPaint);
layout.addView(mView);
}
You need to save and load the data you want to retain. Even though you're handling the screen rotation yourself when you modified the Manifest the way you did, you're still reloading the view yourself. Reread the reference document on Handling Runtime Changes. You need to store your data and reload it accordingly. Otherwise it will be lost when the application restarts or when you reload your ContentView.
http://developer.android.com/guide/topics/resources/runtime-changes.html
You could approach this a few ways.
I assume MyView is your own class which extends View. If so there are two methods which you may care to know, onSaveInstanceState() and onRestoreInstanceState(). When saving you create a parcelable that will contain enough data for you to re-render your view if it were to be destroyed and recreated.
class MyView extends View {
private String mString;
onDraw(Canvas v) { ... }
Parcelable onSaveInstanceState() {
Bundle b = new Bundle();
bundle.putString("STRING", mString);
return b;
void onRestoreInstanceState(Parcelable c) {
Bundle b = (Bundle) c;
mString = bundle.getString("STRING", null);
}
}
Activity has similar state saving mechanics allowed in onCreate and onSaveInstanceState() (inside Activity, not View in this case) which will allow the activity to reset the state of it's view to the state it desires.
This should solve most of your worries. If you are wanting to use the onConfigurationChanged method, then you should reclarify your question as it is not clear what the current behavior is that you aren't expecting in each situation (only using onConfigurationChanged, or only using onCreate, or using both, etc).
I've just used my data-class as singleton (java-pattern).
And it works fine.
--> Application is a Stop-Timer for Racing, where i can stop time from different opponents on the track, so i need the data for longer time, also if the view is repainted.
regz
public class Drivers {
// this is my singleton data-class for timing
private static Drivers instance = null;
public static Drivers getInstance() {
if (instance == null) {
instance = new Drivers();
}
return instance;
}
// some Timer-Definitions.......
}
Then in MainActivity:
// now the class is static, and will alive during application is running
private Drivers drivers = Drivers.getInstance();
#Override
public void onClick(View v) {
if (v == runButton1) {
drivers.startTimer1();
// do some other crazy stuff ...
}
}
// here i put out the current timing every second
private myUpdateFunction(){
time01.setText(drivers.getTimer1());
// update other timers etc ...
}