I am using a View-less Fragment to store some data during orientation change of my Activity. It looks roughly like this:
public class BoardActivity extends BaseActivity {
private DataHandler mDataHandler;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// initialize the data handler
mDataHandler = (DataHandler)mFragmentManager.findFragmentByTag("data");
if (mDataHandler == null) {
mDataHandler = new DataHandler();
mFragmentManager.beginTransaction().add(mDataHandler, "data").commit();
// initialize the data
mDataHandler.mThreads = ...;
} else {
// here, the data is taken and the ListView is filled again.
fillView();
}
}
public static class DataHandler extends Fragment {
private Topic[] mThreads;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
}
What happens is, that when the Activity is left (vor example with the home button) and for some reason is killed in the background, the app crashes upon restart of that Activity. The reason is that although the Fragment mDataHandler is found by the FragmentManager, its Member variable (mThreads) is null.
How come the Fragment itself can be retained but its variables are set to zero?
How come the Fragment itself can be retained but its variables are set to zero?
The fragment was not retained. Retained fragments are retained only for configuration changes. You did not go through a configuration change. Your process was terminated, because Android needed the RAM to support other apps.
Related
I need to fill a Custom Object with data obtained from differents fragments. One option is to do a parcelable object and pass it throught each fragment, but I think that is a better option to declare the object in the activity and access to it from each fragment without move data.
What it is the best option for this?
You can use an interface to communicate between your fragments and activities.
Android documentation explanation is pretty straight forward and with clear examples for what you need, check it here Communicating with Other Fragments
Basically you need to create an interface, declare it in your fragment and your activity should implement it. When you have the data in your fragment you can trigger that event in your activity passing the data and in your activity you will have the data to fill your custom object.
You can store data in a data Fragment, which uses setRetainInstance(true). setRetainInstance controls whether a fragment instance is retained across Activity re-creation (such as from a configuration change).
In Activity class, instantiate the data fragment. It can then be shared across multiple fragments of same activity.
Example data Fragment class:
public class MyDataFragment extends Fragment {
public static MyDataFragment newInstance() {
return new MyDataFragment();
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this instance so it isn't destroyed when MainActivity and MainFragment change configuration.
setRetainInstance(true);
}
// data shared across multiple fragments of same activity
public String myData = null;
}
Example Activity class:
public class MyActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// common data fragment shared by multiple fragments
final FragmentManager fm = getSupportFragmentManager();
MyDataFragment myDataFragment = (MyDataFragment)fm.findFragmentByTag(MY_DATA_FRAGMENT);
if (myDataFragment == null) {
MyDataFragment dataFragment = MyDataFragment.newInstance();
fm.beginTransaction().add(dataFragment, MY_DATA_FRAGMENT).commit();
}
}
public static final String MY_DATA_FRAGMENT = "MY_DATA_FRAGMENT";
In various fragments, you can access the data fragment:
#Override
public void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// access data fragment
final FragmentManager fm = getFragmentManager();
this.myDataFragment = (MyDataFragment)fm.findFragmentByTag(MyActivity.MY_DATA_FRAGMENT);
}
private void function1 () {
// read data from data Fragment
String dataStr = this.myDataFragment.myData;
// write data into data Fragment
this.myDataFragment.myData = "test_string";
}
private MyDataFragment myDataFragment = null;
In my app, I have Fragment which is inside ViewPager. Fragment contains RecyclerView with list of data fetched from web api based on user selection.
On my Fragment onSaveInstanceState I save list data to Bunde, to keep the data on configuration changes etc.
public void onSaveInstanceState(Bundle savedState) {
super.onSaveInstanceState(savedState);
savedState.putParcelableArrayList(LIST_STORAGE_KEY, new ArrayList<>(mItemAdapter.getModels()));
}
Now I have started to see TransactionTooLargeException on my app error reporting.
It seems that in some cases the list which Im putting to Bundle, is too large (as it is collection of quite complex objects).
How should I handle this case? How to store (and restore) my Fragment state.
Is it ok to use setRetainInstance(true) on Fragments inside ViewPager?
To preserve big chunks of data, Google is suggesting to do it with Fragment that retain instance. Idea is to create empty Fragment without view with all necessary fields, that would otherwise been saved in Bundle. Add setRetainInstance(true); to Fragment's onCreate method. And than save data in Fragment on Activity's onDestroy and load them onCreate. Here is and example of Activity:
public class MyActivity extends Activity {
private DataFragment dataFragment;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag("data");
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, "data").commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()
...
}
#Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}
And example of Fragment:
public class DataFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
If you don't want your fragment to use setRetainInstance(true), then you can add an empty fragment with setRetainInstance(true) to your activity. This is useful since child fragments cannot use setRetainInstance(true).
Example:
public class BaseActivity extends Activity {
RetainedFragment retainedFragment;
#Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
retainedFragment = (RetainedFragment) getFragmentManager().findFragmentByTag("retained_fragment");
if (retainedFragment == null) {
retainedFragment = new RetainedFragment();
getFragmentManager().beginTransaction().add(retainedFragment, "retained_fragment").commit();
}
}
public <T> T getState(String key) {
//noinspection unchecked
return (T) retainedFragment.map.get(key);
}
public void saveState(String key, Object value) {
retainedFragment.map.put(key, value);
}
public boolean has(String key) {
return retainedFragment.map.containsKey(key);
}
public static class RetainedFragment extends Fragment {
HashMap<String, Object> map = new HashMap<>();
#Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
}
Then, in your fragment, you can cast getActivity() to your Activity class and use saveState(String, Object) and getState(String) to save your list.
There are other discussions on this which can be found at the following locations:
What to do on TransactionTooLargeException
android.os.TransactionTooLargeException on Nougat (Accepted answer suggests setRetainInstance(true)).
setRetainInstance() is the best way to achieve that without side effects. Using static will cause memory leak and there is no use of saving the state in onSaveInstanceState() and getting it back since, setRetainInstance() does that for you.
So create a field for the list in fragment class and always check for null or size of list to begin operation of fetching latest data
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;
}
}
got the following example while reading android book. Can somebody please confirm to me why adapter is always created in this example? Shouldn't it be done only in the case when model == null?
If I understand correctly all data members are retained (in this example), so ListView will be retained, along with its configured ListAdapter and everything else.
public class AsyncDemoFragment extends SherlockListFragment {
private static final String[] items = { "lorem", "ipsum", "dolor" };
private ArrayList<String> model = null;
private ArrayAdapter<String> adapter = null;
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
if (model == null) {
model = new ArrayList<String>();
new AddStringTask().execute();
}
adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, model);
setListAdapter(adapter);
}
class AddStringTask extends AsyncTask<Void, String, Void> {
// …
protected void onProgressUpdate(String... item) {
adapter.add(item[0]);
}
}
}
The instance of your Fragment will be retained -- however, the View created by the Fragment will still be destroyed and recreated unless specifically retained (which can very easily cause memory leaks). Basically, without setRetainInstance(), the following events (along with others) would happen on a configuration change:
// Fragment initialized
onCreate()
onCreateView()
// Configuration change
onDestroyView()
onDestroy()
onCreate()
onCreateView()
With setRetainInstance(true):
// Fragment initialized
onCreate()
onCreateView()
// Configuration change
onDestroyView()
onCreateView()
Essentially, you still need to recreate the View, but any other instance fields will not be reset.
You should still be able to handle the case where they are reset, however, as even with setRetainInstance(true) your Activity may be killed in the background due to memory pressure.
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 ...
}