I'm currently testing my app with a multipane Fragment-ised view using the HC compatibility package, and having a lot of difficultly handling orientation changes.
My Host activity has 2 panes in landscape (menuFrame and contentFrame), and only menuFrame in portrait, to which appropriate fragments are loaded. If I have something in both panes, but then change the orientation to portrait I get a NPE as it tries to load views in the fragment which would be in the (non-existent) contentFrame. Using the setRetainState() method in the content fragment didn't work. How can I sort this out to prevent the system loading a fragment that won't be shown?
Many thanks!
It seems that the onCreateViewMethod was causing issues; it must return null if the container is null:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (container == null) // must put this in
return null;
return inflater.inflate(R.layout.<layout>, container, false);
}
Probably not the ideal answer but if you have contentFrame for portrait and in your activity only load up the menuFrame when the savedInstanceState is null then your content frame fragments will be shown on an orientation change.
Not ideal though as then if you hit the back button (as many times as necessary) then you'll never see the menu fragment as it wasn't loaded into contentFrame.
It is a shame that the FragmentLayout API demos doesn't preserve the right fragment state across an orientation change. Regardless, having thought about this problem a fair bit, and tried out various things, I'm not sure that there is a straightforward answer. The best answer that I have come up with so far (not tested) is to have the same layout in portrait and landscape but hide the menuFrame when there is something in the detailsFrame. Similarly show it, and hide frameLayout when the latter is empty.
Create new Instance only for First Time.
This does the trick:
Create a new Instance of Fragment when the
activity start for the first time else reuse the old fragment.
How can you do this?
FragmentManager is the key
Here is the code snippet:
if(savedInstanceState==null) {
userFragment = UserNameFragment.newInstance();
fragmentManager.beginTransaction().add(R.id.profile, userFragment, "TAG").commit();
}
else {
userFragment = fragmentManager.findFragmentByTag("TAG");
}
Save data on the fragment side
If your fragment has EditText, TextViews or any other class variables
which you want to save while orientation change. Save it
onSaveInstanceState() and Retrieve them in onCreateView() method
Here is the code snippet:
// Saving State
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("USER_NAME", username.getText().toString());
outState.putString("PASSWORD", password.getText().toString());
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.user_name_fragment, parent, false);
username = (EditText) view.findViewById(R.id.username);
password = (EditText) view.findViewById(R.id.password);
// Retriving value
if (savedInstanceState != null) {
username.setText(savedInstanceState.getString("USER_NAME"));
password.setText(savedInstanceState.getString("PASSWORD"));
}
return view;
}
You can see the full working code HERE
Related
I have read a lot of things about the data save instance and restore but Unable to implement in my case. What is my case in application .
I am using the Activity (MainActivity) and calling the fragment in it let say ParentFragment
ParentFragment Calls the ChildFragment in it though ParentFragment has its own views such as TextViews which takes First name, Last name and age and below this part I am programatically calling the ChildFragment
So in this way I have 2 fragments in the MainActivity, which is being shown to the user at a time on the screen
**What I want **
I want when User has change the orientation the layout should also change but I also want that the text fields should maintain there data in them .
I want to save some more fragment variables and their data also on configuration changes and retrieve them after the new layout is set after screen orientation changed.
**Problems and Confusions **
I have no idea If i set the Fragment.setRetainInstance(true) then would my fragment still be able to receive the onConfiguration Change call back?
When I rotate my device, the fragment gets re-initialize and also my activity has the Asynctask and that runs again , i want my activity to hold the same data . How can I achieve that?
Please help me and give me some hint.
If you want to change layout on orientation change then,you should not handle configuration change by retaining the activity(by setting android:configChanges="orientation" value for corresponding activity defined in manifest) or by using setRetainInstance() in fragment.
For saving the fragment state on configuration change use
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(FIRST_NAME_VALUE_KEY, firstNameTextView.getText());
outState.putInt(LAST_NAME_VALUE_KEY, lastNameTextView.getText());
super.onSaveInstanceState(outState);
}
and for writing back state to the views in fragment use
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.my_fragment, container, false);
if (savedInstanceState != null) {
String firstName = savedInstanceState.getInt(FIRST_NAME_VALUE_KEY);
String lastName = savedInstanceState.getInt(LAST_NAME_VALUE_KEY);
firstNameTextView.setText(firstName);
lastNameTextView.setText(lastName);
}
return view;
}
You will receive onConfigurationChanged() callback by retaining activity.it is not related to Fragment.setRetainInstance(true).
To avoid running asynctask again,We can store data and retain it using following callbacks inside the activity.
#Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
// Save custom values into the bundle
savedInstanceState.putInt(SOME_VALUE, someIntValue);
savedInstanceState.putString(SOME_OTHER_VALUE, someStringValue);
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
in onCreate(Bundle savedInstanceState) call back you can check for savedInstanceSate , based on that you can call u asynctask or retain values as below
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
asyncTask.execute();
} else {
someIntValue = savedInstanceState.getInt(SOME_VALUE);
someStringValue = savedInstanceState.getString(SOME_OTHER_VALUE);
}
}
I have written android app now for a long time but now I'm facing a problem that I have never thought about. It is about the android lifecycle of Activitys and Fragments in in relation to configuration changes. For this I have create a small application with this necessary code:
public class MainActivity extends FragmentActivity {
private final String TAG = "TestFragment";
private TestFragment fragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
fragment = (TestFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new TestFragment();
fm.beginTransaction().add(R.id.fragment_container, fragment, TAG).commit();
}
}
}
And here is my code for the TestFragment. Note that I'm calling setRetainInstance(true); in the onCreate method so the fragment is not recrated after a configuration change.
public class TestFragment extends Fragment implements View.OnClickListener {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater li, ViewGroup parent, Bundle bundle) {
View rootView = li.inflate(R.layout.fragment_test, parent, false);
Button button = (Button) rootView.findViewById(R.id.toggleButton);
button.setOnClickListener(this);
return rootView;
}
#Override
public void onClick(View v) {
Button button = (Button) v;
String enable = getString(R.string.enable);
if(button.getText().toString().equals(enable)) {
button.setText(getString(R.string.disable));
} else {
button.setText(enable);
}
}
}
And here is the layout that my fragment is using:
<LinearLayout
...>
<EditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="#+id/toggleButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/enable"/>
</LinearLayout>
My problem is that if I rotate the device the text of the Button change back to the default value. Of course the View of the Fragment is new created and inflated but the saved instance for the Views should be restored. I also have an EditText in my layout and there the text and other properties remains after the rotation. So why is the Button not restore from the Bundle by default? I have read on the developer site:
By default, the system uses the Bundle instance state to save information about each View object in your activity layout (such as the text value entered into an EditText object). So, if your activity instance is destroyed and recreated, the state of the layout is restored to its previous state with no code required by you.
I've also read a lot of answers the last days but I do not know how actual they are anymore. Please do not leave a comment or an answer with android:configChanges=... this is very bad practice. I hope someone can bring light into my lack of understanding.
You should save state of your fragment in the onSaveInstanceState(Bundle outState) and restore it in the onViewCreated(View view, Bundle savedState) method. This way you will end up with the UI just as it was before configuration change.
TextView subclasses don't save their text by default. You need to enable freezesText="true" in the layout, or setFreezesText(true) at runtime for it to save its state.
As per documentation, views should maintain their state without using setRetainInstance(true). Try to remove it from your onCreate, this should force the fragment to be recreated on screen rotation, hence all of it's views should be saved before rotation and restored after.
This stack overflow should answer your question:
setRetainInstance not retaining the instance
setRetainInstance does tell the fragment to save all of its data, and for ui elements where the user has manipulated the state (EditText, ScrollView, ListView, etc) the state gets restored. That being said, normal read-only UI components get reinflated from scratch in onCreateView and have to be set again - my guess would be that their properties are not considered "data" that needs to be retained and restored - Google probably does this for performance reasons. So things like a normal Button, ImageView, or TextView need their contents set manually when they are reinflated if it differs from the initial state in the XML. (TextView's android:freezesText basically puts the TextView in a mode that uses an implementation to save it's state and restore it).
PS: According to this stack overflow Save and restore a ButtonText when screen orientation is switched, you may be able to set android:freezesText on the button to have it keep the text you set - I haven't tried it, but it makes sense.
Edit after Op feedback
Although this fails to answer the question, after consultation with the OP, I've decided to leave it here as a reference point of information on the topic for people who land here. Hope you find it helpful.
Try putting your setRetainInstance in onCreateView. See here
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater li, ViewGroup parent, Bundle bundle) {
setRetainInstance(true);
View rootView = li.inflate(R.layout.fragment_test, parent, false);
Button button = (Button) rootView.findViewById(R.id.toggleButton);
button.setOnClickListener(this);
return rootView;
}
Called when the fragment's activity has been created and this
fragment's view hierarchy instantiated. It can be used to do final
initialization once these pieces are in place, such as retrieving
views or restoring state. It is also useful for fragments that use
setRetainInstance(boolean) to retain their instance, as this
callback tells the fragment when it is fully associated with the new
activity instance. This is called after onCreateView(LayoutInflater,
ViewGroup, Bundle) and before onViewStateRestored(Bundle).
developer.android.com/reference/android/app/Fragment.html#onActivityCreated
Control whether a fragment instance is retained across Activity
re-creation (such as from a configuration change). This can only be
used with fragments not in the back stack. If set, the fragment
lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because
the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being
re-created.
onAttach(Activity) and onActivityCreated(Bundle) will
still be called.
developer.android.com/reference/android/app/Fragment.html#setRetainInstance
And taken from here:
onCreate : It is called on initial creation of the fragment. You do
your non graphical initializations here. It finishes even before the
layout is inflated and the fragment is visible.
onCreateView : It is called to inflate the layout of the fragment i.e
graphical initialization usually takes place here. It is always called
sometimes after the onCreate method.
onActivityCreated : If your view is static, then moving any code to
the onActivityCreated method is not necessary. But when you - for
instance, fill some lists from the adapter, then you should do it in
the onActivityCreated method as well as restoring the view state when
setRetainInstance used to do so. Also accessing the view hierarchy of
the parent activity must be done in the onActivityCreated, not sooner.
Let me know if this helps.
So I have an activty with 3 tabs which I navigate between through the ActionBar (SupportActionBar). Each of the tabs has a Fragment with a WebView attached to them, with a TabListener implement as examplified in the Android Docs.
This all works fine, except that the onCreateView method of the Fragment is called each time a fragment is reattached. This in turn causes the WebView to either (1) be blank or (2) reload, if I call restoreState() on it (which I previously have saved manually).
I don't want the page to reload each time the user switches tabs. Neither do I want the scrollbar to reset. Or the HTML forms (if any) to be reset. How can I accomplish this?
Solved by keeping a reference to the WebView in the fragment class, and in onCreateView I do this:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (mWebView == null) {
mWebView = new WebView(container.getContext());
mWebView.restoreState(savedInstanceState);
}
else {
View parent = mWebView.getParent();
if (parent != null && parent instanceof ViewGroup)
((ViewGroup) parent).removeView(mWebView);
}
return mWebView;
}
I'm making an app with webviews.
I just have a single activity containing
a FrameLayout to dynamically add fragments.
Every fragment contains a webView.
Everything works fine, but when I remove a fragment
from the stack, the webview of the fragment in the
top of the stack is reloaded so the content inside the
script of the html is called again.
The restoreState() method is not working.
This is the onCreateView of the fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.wv_container, container, false);
webView = (WebView)view.findViewById(R.id.webviewcontainer);
if(webViewBundle == null)
webView.loadUrl("file:///android_asset/www/main.html");
else
webView.restoreState(webViewBundle);
return view;
}
This is the onPause() method:
#Override
public void onPause() {
super.onPause();
webViewBundle = new Bundle();
webViewContent.saveState(webViewBundle);
}
How should I prevent the page reloading for my case?
I don't want to hack the html with flags or something like that.
Thanks in advance!
The saveState method in your onPause returns a WebBackForwardList, which contains a list of WebHistory items. These are basically the titles, urls and favicons from recently visited pages. It does not cache the contents of the downloaded url.
Try looking at the method saveWebArchive instead.
I have an app with hierarchy like this:
FragmentTabHost (Main Activity)
- Fragment (tab 1 content - splitter view)
- Fragment (lhs, list)
- Framment (rhs, content view)
- Fragment (tab 2 content)
- Fragment (tab 2 content)
All fragment views are being inflated from resources.
When the app starts everything appears and looks fine. When I switch from the first tab to another tab and back again I get inflate exceptions trying to recreate tab 1's views.
Digging a little deeper, this is what's happening:
On the first load, inflating the splitter view causes its two child fragments to be added to the fragment manager.
On switching away from the first tab, it's view is destroyed but it's child fragments are left in the fragment manager
On switching back to the first tab, the view is re-inflated and since the old child fragments are still in the fragment manager an exception is thrown when the new child fragments are instantiated (by inflation)
I've worked around this by removing the child fragments from the fragment manager (I'm using Mono) and now I can switch tabs without the exception.
public override void OnDestroyView()
{
var ft = FragmentManager.BeginTransaction();
ft.Remove(FragmentManager.FindFragmentById(Resource.Id.ListFragment));
ft.Remove(FragmentManager.FindFragmentById(Resource.Id.ContentFragment));
ft.Commit();
base.OnDestroyView();
}
So I have a few questions:
Is the above the correct way to do this?
If not, how should I be doing it?
Either way, how does saving instance state tie into all of this so that I don't lose view state when switching tabs?
I'm not sure how to do this in Mono, but to add child fragments to another fragment, you can't use the FragmentManager of the Activity. Instead, you have to use the ChildFragmentManager of the hosting Fragment:
http://developer.android.com/reference/android/app/Fragment.html#getChildFragmentManager()
http://developer.android.com/reference/android/support/v4/app/Fragment.html#getChildFragmentManager()
The main FragmentManager of the Activity handles your tabs.
The ChildFragmentManager of tab1 handles the split views.
OK, I finally figured this out:
As suggested above, first I changed the fragment creation to be done programatically and had them added to the child fragment manager, like so:
public override View OnCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstance)
{
var view = inflater.Inflate(Resource.Layout.MyView, viewGroup, false);
// Add fragments to the child fragment manager
// DONT DO THIS, SEE BELOW
var tx = ChildFragmentManager.BeginTransaction();
tx.Add(Resource.Id.lhs_fragment_frame, new LhsFragment());
tx.Add(Resource.Id.rhs_fragment_frame, new RhsFragment());
tx.Commit();
return view;
}
As expected, each time I switch tabs, an extra instance of Lhs/RhsFragment would be created, but I noticed that the old Lhs/RhsFragment's OnCreateView would also get called. So after each tab switch, there would be one more call to OnCreateView. Switch tabs 10 times = 11 calls to OnCreateView. This is obviously wrong.
Looking at the source code for FragmentTabHost, I can see that it simply detaches and re-attaches the tab's content fragment when switching tabs. It seems the parent Fragment's ChildFragmentManager is keeping the child fragments around and automatically recreating their views when the parent fragment is re-attached.
So, I moved the creation of fragments to OnCreate, and only if we're not loading from saved state:
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (savedInstanceState == null)
{
var tx = ChildFragmentManager.BeginTransaction();
tx.Add(Resource.Id.lhs_fragment_frame, new LhsFragment());
tx.Add(Resource.Id.rhs_fragment_frame, new RhsFragment());
tx.Commit();
}
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstance)
{
// Don't instatiate child fragments here
return inflater.Inflate(Resource.Layout.MyView, viewGroup, false);
}
This fixed the creation of the additional views and switching tab's basically worked now.
The next question was saving and restoring view state. In the child fragments I need to save and restore the currently selected item. Originally I had something like this (this is the child fragment's OnCreateView)
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
var view = inflater.Inflate(Resource.Layout.CentresList, container, false);
// ... other code ommitted ...
// DONT DO THIS, SEE BELOW
if (savedInstance != null)
{
// Restore selection
_selection = savedInstance.GetString(KEY_SELECTION);
}
else
{
// Select first item
_selection =_items[0];
}
return view;
}
The problem with this is that the tab host doesn't call OnSaveInstanceState when switching tabs. Rather the child fragment is kept alive and it's _selection variable can be just left alone.
So I moved the code to manage selection to OnCreate:
public override void OnCreate(Bundle savedInstance)
{
base.OnCreate(savedInstance);
if (savedInstance != null)
{
// Restore Selection
_selection = savedInstance.GetString(BK_SELECTION);
}
else
{
// Select first item
_selection = _items[0];
}
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
// Don't restore/init _selection here
return inflater.Inflate(Resource.Layout.CentresList, container, false);
}
Now it all seems to be working perfectly, both when switching tabs and changing orientation.