setRetainInstance with Fragment - android

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.

Related

Wait for event when fragments fully initialized in FragmentPagerAdapter to setup them with data

I have an activity with collapsing AppBarLayout. In onCreate() method I am sending request to server to get some data. And depending what data I get - I need to dynamically in runtime choose what view to show to the user: 1. MyFragment1; or 2. TabLayout/ViewPager with FragmentPagerAdapter, which has two fragments in it. And I need to set some data to that fragments. But the issue is in next: I already have data and set it to fragments in my adapter, but fragment method onCreate is not yet called, and my layout is not initialized. That's how I get crash on populating data into layout view. So, how can I make somehow - fragment created and initialized it's fields first and only then setup it with data? Thanks.
private MenuFragment1 menu1Fragment1;
private MenuFragment3 menu1Fragment3;
private TabMenuAdapter adapter;
private void setupViewPager(ViewPager viewPager) {
menu1Fragment1 = new MenuFragment1();
menu1Fragment3 = new MenuFragment3();
adapter = new TabMenuAdapter(getSupportFragmentManager());
adapter.addFragment(menu1Fragment1, "Menu 1");
adapter.addFragment(menu1Fragment3, "Menu 2");
viewPager.setAdapter(adapter);
}
public onDataLoaded(String data)
{
//at this point, fragment is created, but it's View fields are NULL!!
menu1Fragment1.data = data;
}
#Layout(id = R.layout.content_shop_final)
public class ShopFinalTermsFragment extends BaseFragment {
private static final String SANS_SERIF_FAMILY_NAME = "sans-serif";
private static final String SANS_SERIF_MEDIUM_FAMILY_NAME = "sans-serif-medium";
private InfoModel InfoModel;
private RateModel RateModel;
#BindView(R.id.shop_final_nested_scroll_view)
NestedScrollView nestedScrollView;
#BindView(R.id.shop_final_pending_txt)
TextView pendingDurationTxt;
#BindView(R.id.shop_final_rate_cond_rv)
RecyclerView rateCondRv;
#BindView(R.id.shop_final_description_txt)
TextView descriptionTxt;
#Inject
ToolsManager toolsManager;
RateConditionsAdapter adapter;
private String getParams;
public static ShopFinalTermsFragment newInstance(String getParams, InfoModel shopInfoModel, RateModel RateModel) {
ShopFinalTermsFragment fragment = new ShopFinalTermsFragment();
Bundle args = new Bundle();
args.putString(SHOP_GET_PARAMS, shopGetParams);
args.putSerializable(INFO_MODEL_KEY, shopInfoModel);
args.putSerializable(MODEL_KEY, userCashbackRateModel);
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
this.GetParams = getArguments().getString(SHOP_GET_PARAMS);
this.InfoModel = (InfoModel) getArguments().getSerializable(INFO_MODEL_KEY);
this.RateModel = (RateModel) getArguments().getSerializable(RATE_MODEL_KEY);
}
}
#Override
protected void setupInOnCreateView() {
nestedScrollView.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
conditionsTxt.setTypeface(Typeface.create(SANS_SERIF_FAMILY_NAME, Typeface.BOLD));
} else {
conditionsTxt.setTypeface(Typeface.create(SANS_SERIF_MEDIUM_FAMILY_NAME, Typeface.NORMAL));
}
}
#Override
protected void inject() {
ShopsComponent shopsComponent = DaggerShopsComponent.builder()
.applicationComponent(((BaseActivity) getActivity()).getApplicationComponent())
.build();
shopsComponent.inject(this);
}
public void setupWithData(InfoModel InfoModel, RateModel RateModel) {
//THIS METHOD IS COLLED FROM ACTIVITY'S onDataLoaded(InfoModel InfoModel, RateModel RateModel) method
setupShopInformation(shopInfoModel);
setCashBackRateModel(userCashbackRateModel);
}
}
You are using the dependency in a wrong way. It's not the activity that should call setupWithData on a fragment but it should be a fragment getting data from the activity (or other storage) instead. This way you will break this dependency on the fragment lifecycle which ends up being uninitialized.
Get the data from the server, store it where you need to, and update the UI from your activity. At this point you either show MyFragment1 or your TabLayout/ViewPager. If it's a TabLayout or a ViewPager, all you do is creating fragments and adding the to the layout or a corresponding pager adapter. That's it. You don't set the data at this point.
Now when your inner fragments populate in the pager adapter, they will go through onAttach, onCreate, onStart and onResume lifecycle methods. onResume is a good place to load the data. You either access it directly from the fragment, or get it from your outbound activity - depends on what makes more sense for you. If you need an activity reference, you can access it via getActivity() method in the fragment.
So in the fragment's onResume you will have something like:
setupShopInformation((YourActivity) getActivity()).getShopInfoModel());
setCashBackRateModel((YourActivity) getActivity()).getUserCashbackRateModel());
Although it would be even better to have it stored in some state class. But that will be a separate question.
Good luck!

Fragment, save large list of data on onSaveInstanceState (how to prevent TransactionTooLargeException)

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

Where and how to do network call when using fragments

I am building an app that has fragments. Im not entirely sure when to make a http nw call to get data to populate components within these fragments. I have a call that gets all the data i need for all my fragments to draw the screen. I have tested that a async task gets that data ok but in the postExecute i don't have access to components unless im in the fragment class, even then how would i put it in there.
I can do a asyncTask and see my data coming back but where do i put this, i want to make the call once.
ActivityTileData.getLoginTileDataArray(getActivity()); currently returns dummy data, ideally this would do the network call.
public class StaggeredGridActivityFragment extends FragmentActivity {
private static final String TAG = "StaggeredGridActivityFragment";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.requestWindowFeature(Window.FEATURE_NO_TITLE); //remove title bar
final FragmentManager fm = getSupportFragmentManager();
// Create the list fragment and add it as our sole content.
if (fm.findFragmentById(android.R.id.content) == null) {
final StaggeredGridFragment fragment = new StaggeredGridFragment();
fm.beginTransaction().add(android.R.id.content, fragment).commit();
}
}
private class StaggeredGridFragment extends Fragment implements
AbsListView.OnScrollListener, AbsListView.OnItemClickListener {
private StaggeredGridView mGridView;
private boolean mHasRequestedMore;
private TilesAdapter mAdapter;
private ArrayList<String> mData;
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
return inflater.inflate(R.layout.activity_sgv, container, false);
}
#Override
public void onActivityCreated(final Bundle savedInstanceState) {
//Encapsulate all within a post cereate from a async task or call a blocking http call
super.onActivityCreated(savedInstanceState);
mGridView = (StaggeredGridView) getView().findViewById(R.id.grid_view);
if (savedInstanceState == null) {
final LayoutInflater layoutInflater = getActivity().getLayoutInflater();
View header = layoutInflater.inflate(R.layout.list_item_header_footer, null);
mGridView.addHeaderView(header);
}
if (mAdapter == null) {
mAdapter = new TilesAdapter(getActivity(), R.id.summary1_value);
}
if (mData == null) {
mData = ActivityTileData.getLoginTileDataArray(getActivity());
}
for (String data : mData) {
mAdapter.add(data); //Add each mData TileAdapter element to an mAdapter where it will be further broken down and used by the TileAdapter
}
mGridView.setAdapter(mAdapter);
mGridView.setOnScrollListener(this);
mGridView.setOnItemClickListener(this);
}
I would recommend to do n/w call in onCreate of your activity and make sets of data for your each fragment and put them in arguments of fragment. You can load the data in newInstance of fragment by getting required data from bundle.I have done one project of this type and it worked for me.
Note: The above method works if the data doesn't need to change once it has been set.
Do not make the async call inside your fragments and/or activities. If you do, you will get many issues handling config changes (like when the user rotates the screen). For my experience, what I usually do is to create a class called Loader (not the Android framework Loader class, it is an overkill for this purpose) and place your AsyncTask or loader thread there. From your fragment/activity get a reference to that class that should be implemented as a singleton (override your Application class for this) on its onCreate(). Then, call loader.loadDate() whenever you need it. If you want to handle config changes, create an observable pattern in your Loader class, so fragments and activities can register to hear loading events such as loadFinished(data) or loadError(error). Also, if you dont want to query for data everytime a config change happens, set a catched variable inside your loader that holds the previous loaded data.

setRetainInstance(true) does not persist member variables of the Fragment

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.

View was clear data when screen rotation in Android

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 ...
}

Categories

Resources