Android navigation tabs: Restoring fragment view state - android-fragments

I'm trying to understand how to preserve fragment view state when fragments are used within navigation tabs. In my efforts, I've come across two problems that I cannot find any proper solutions to.
I have two tabs, Tab1 and Tab2. Tab1's layout is defined by FragmentA and Tab2's layout is defined by FragmentB.
I've followed the approach given here (edit: documentation has changed since this question was asked).
The first problem: Even though my views have IDs, their states are not fully restored when a fragment is re-attached (after a tab switch rotation). In particular: an EditText with an ID does indeed save its entered text, but it does not save its enabled status. Also, buttons do not save if they are enabled or disabled even though they have IDs. I've found two possible workarounds for this problem:
Use hide()/show() instead of attach()/detach() when switching tabs.
in onPause(), save the current fragment view state in a View instance variable of the fragment via getView(). In onCreateView(Bundle savedInstanceState) check if this field is non-null and if that is the case return the value of this field. This solution seems hacky, and I've been told that it might also introduce a memory leak in my app.
The second problem: Consider the following user interaction:
User starts on Tab1 and does some changes that put the view state of Tab1 in a different state than its default state (and we want the fragment to save this view state through tabswitches and device tilts).
User then goes to Tab2. User then tilts her/his device (still at Tab2).
User then swaps to Tab1 (at the new screen orientation).
Now, the problem is: when the user initially swaps from Tab1 to Tab2, the fragment is detached and thereby its view discarded (even though the fragment instance still lives). When the user then tilts the device, the activity - and thereby both FragmentA and FragmentB associated with it - are destroyed. Since FragmentA at this point does no longer have a view (remember: it was detached), we cannot save the state of its view elements (e.g., what buttons are enabled/disabled) during the call to FragmentA.onSaveInstanceState(Bundle savedInstanceState). How do you recover fragment view state in a situation like this? Is the only viable solution to save every single view element's different status flags as SharedPreferences? This seems way too complicated for such an "everyday job".

Problem 1:
Android does not save your view enabled state by default. It seems only things which are directly influenced by user actions (without additional code) are saved. For a normal View, no information is saved, and for a TextView, of which EditText is a subclass, the entered text is saved (if freezesText is set).
If you want to anything else to be saved, you will have to do it yourself. Here is an question with some answers that show how to implement custom view state saving. You can stick with attach/detach if you follow that approach.
Problem 2:
You are right in that Fragment.onSaveInstanceState(Bundle) can be called after your view has already been destroyed. However, this is not where you should be saving your view state. Android will call View.onSaveInstanceState() right before it destroys your views when detaching a fragment. It saves this state and gives it back to you when you attach the fragment again. This is exactly what happens when you flip between tabs normally with no rotations. Fragment.onSaveInstanceState(Bundle) is not called when detaching. Even if you rotate the device, the view state saved as a result of the detach will persist. If you implement View.onSaveInstanceState() as directed above, your view state will be saved and restored properly, even in the Tab1-Tab2-rotate-Tab1 scenario.
Side note:
The example code in the docs seems to have some problems when you try to rotate. The lifetime of the TabListener is the same as that of the Activity - a new one is created every time you rotate. This means it also loses its internal reference to the fragment, every time you rotate. Added fragments are recreated automatically and so there is no need for the TabListener to try to create a new instance and add it, after rotation. Instead, of the internal reference, it should just try to find the fragment with the appropriate tag in the fragment manager. After rotation it will still exist.
The other problem is with that the selected tab is not saved, but this is noted at the bottom of the example. You could save this in Activity.onSaveInstanceState(Bundle).

private ViewPager viewPager;
viewPager = (ViewPager) findViewById(R.id.pager);
mAdapter = new TabsPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(mAdapter);
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
// on changing the page
// make respected tab selected
actionBar.setSelectedNavigationItem(position);
}
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
#Override
public void onPageScrollStateChanged(int arg0) {
}
});
}
#Override
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
// on tab selected
// show respected fragment view
viewPager.setCurrentItem(tab.getPosition());
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
}

Related

Detect a change of iframe in page detail view in magnolia

Is there a way to detect which node is being rendered when the user selects a link within the pages detail app and only the iframe changes? If just the the iframe is changing I need a mechanism that can call the parent page and send the path of the node that is being rendered in the iframe. We run Magnolia EE 5.6.11.
My issue is that I have a ValueChangeListener in a ComboBox that I use as a versionSelector in a PageBar extension in the pages detail app.
// Create a selection component;
private ComboBox versionSelector = new ComboBox();
private Listener listener;
private boolean isSettingValue;
public VersionSelectorViewImpl(){
construct();
}
private void construct() {
versionSelector.setVisible(false);
versionSelector.setSizeUndefined();
versionSelector.setImmediate(true);
versionSelector.setNullSelectionAllowed(false);
versionSelector.setTextInputAllowed(false);
//setup listener for the selected item
versionSelector.addValueChangeListener(new Property.ValueChangeListener() {
#Override
public void valueChange(Property.ValueChangeEvent event) {
if (listener != null) {
listener.versionSelected((Object) event.getProperty().getValue());
}
}
});
}
The implementation is similar to the LanguageSelector or VariantSelector. When someone activates a hyperlink in the page template the iframe changes and the valueChange method retrieves the wrong value (ie. the event is from the previous page).
When someone activates a link, the PagesEditorSubApp#updateNodePath calls the updateLocationDependentComponents which calls the PageBar.onLocationUpdate. This calls in our case the VersionSelector#setCurrentVersion method. At this point I would need to reload the page detail subapp so that the listener is correctly set to the new page. I tried using the pageEditorPresenter.refresh() method in the setCurrentVersion method but it didn't do it.
In Magnolia 5.x you need to fire ContentChangeEvent to trigger the refresh (and hope that subapp you want to refresh listens to it, which in case of page detail subapp shouldn't be a problem).
Or, since you mention LanguageSelector, you can indeed try to call PageEditorPresenter directly as it does from info.magnolia.pages.app.editor.pagebar.languageselector.LanguageSelector#languageSelected, tho unless you need to modify something in the presenter (eg. locale in the example above), it seems rather unnecessary coupling of the code.
Once you are migrating your code to Magnolia 6.x and new UI framework, you have more convenient way of triggering notification of datasource change which all the views consuming data from given source listen to and facilitating refresh that way (as is shown eg in PasteComponentAction here.

OnNavigatedTo or Initialize too late for binding

I'm new to Prism with Xamarin.Forms. I've implemented the ViewModel approach using the Navigation and Commanding classes. It works just fine but there's just one thing I don't understand. With the AutowireViewModel set to true the ViewModelLocator automatically fills the BindingContext for me and that is sweet. The order of things is not what I expected. First the binding for the properties on the View fire and then the OnNavigatedTo is fired. This means that my init of the properties is already finished by the time I receive the parameters on the View. I can solve this by executing the RaisePropertyChanged. This causes the following:
I'm forced to write RaisePropertyChanged for every property on the ViewModel I want to see on the view with the new Data.
All bindings fire two times. For fast stuff that's not a problem but some are slower.
Data is refreshed after the View has become visible. Not disturbing but it would be nicer to show the finished View with it's data all at once.
All properties must be able to handle null references.
Is there a way to initialize the data in the ViewModel before the binding kicks in?
Actually Prism for Xamarin.Forms has long supported initializing your ViewModels prior to the View being pushed onto the NavigationStack. That said there is an order of operations that has to be taken into consideration. We cannot for instance perform a bunch of operations on a ViewModel and then attach it to a View.
The Order in which things are carried out are as follows:
The View is Created (Anything in the View's ctor is executed)
If you have specifically attached the ViewModelLocator.AutowireViewModel property this will resolve the ViewModel as part of the ctor
If you have not specifically opted out of the ViewModelLocator's Autowire, the Navigation Service will set it for you (after the ctor has completed)
The NavigationService will then call IAutoInitialize/IInitialize/InitializeAsync (for Prism 7.2+... INavigatingAware.OnNavigatingTo in older versions of Prism)
The NavigationService will then push the Page onto the Navigation Stack (note that this may or may not be visible to the user as additional pages may have to be added first when deep linking)
The NavigationService will then call OnNavigatedFrom / OnNavigatedTo (this is where people often report seeing a visible delay due to binding updates.
For an overwhelming number of scenarios if you have correctly initialized your ViewModel this process works exactly as you need. If you have some edge case where you absolutely have to ensure the ViewModel is initialized before it is set then you will need to handle this manually.
public partial class ViewA : ContentPage, IInitialize
{
private ViewAViewModel ViewModel { get; }
public ViewA(ViewAViewModel viewModel)
{
ViewModel = viewModel;
// Explicitly Opt Out of Autowiring
ViewModelLocator.SetAutowireViewModel(this, false);
InitializeComponent();
}
public void Initialize(INavigationParameters parameters)
{
ViewModel.Initialize(parameters);
BindingContext = ViewModel;
}
}

Avoid back button using vaadin

I develop RIA application using Vaadin CDI addon.
About the app : it has two views(viewA and viewB) which maintained by Navigator.
User story is pretty simple:
User enters viewA
Perform some business stuff
Redirects to viewB
Using address bar to go some external website(ex. google.com)
Press back and it goes to he lastest page what he saw(viewB), instead of viewA
Any suggestions/tips-n-tricks how to avoid redirecting to viewB, but redirect to viewA?
Vaadin Forum quiet about this thing.
Thank you
I had same problem but resolved using following code:
getUI().getNavigator().addViewChangeListener(new ViewChangeListener() {
public boolean beforeViewChange(ViewChangeEvent event) {
return changeView;
}
#Override
public void afterViewChange(ViewChangeEvent event) {
// TODO Auto-generated method stub
}});
Set changeView = true only through proper navigation's (eg: On button clicks).
This how you can avoid navigation's using browser back button. In this case if user uses any of browser buttons the view does not changes and it remains on same page.
You could override View.enter(...) in your ViewB, and, according to your application state, update your view URI using Page.getCurrent().setUriFragment(recentUrl, false);

Android prevent webview reload in fragment

I have a main activity that contains a frame layout and 4 tab buttons, and there are in total 4 fragments, each fragment relates to a tab button, depending on which tab button is clicked, the corresponding fragment will be swapped into the frame layout to replace the old one. Now in each of the fragment there's a webview that loads different URL when the fragment is created. My question is how to prevent from the webview to reload every time when fragments swap? Below are the code how I swap my fragment:
FragmentMainPage fragment = new FragmentMainPage();
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.replace(R.id.frameLayout, fragment);
transaction.commit();
currentPage = VT_Constants.HOME_PAGE;
Calling replace is destructive: it tears down the fragment's view hierarchy, so when you add it back later, the views have to be rebuilt, which explains the behavior you are seeing.
Try show and hide instead. These maintain the fragment's views, so they can be re-attached to your activity when the user clicks on that fragment's tab. Something like this (although I'm not sure how you want to get the reference to the current fragment):
FragmentMainPage fragment = new FragmentMainPage();
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
Fragment oldFragment = fm.findFragmentByTag(...); // somehow get the current fragment showing
FragmentTransaction transaction = fm.beginTransaction();
transaction.hide(oldFragment);
transaction.show(fragment);
transaction.commit();
You can set page limit.
viewPager.setOffscreenPageLimit(4);
Webviews won't reload anymore.
I was also facing the same problem. Tried changing the webView Settings and none worked for me. This is not the problem with the webview.
The actual problem here is your fragment is a part of ViewPager. By default viewPager will have setOffscreenPageLimit as 1.
For one immediate tab switch, you will not see any refresh of the fragment. Post that you will face this issue.
You have to simply add this one line to your view pager if you don't want to refresh fragments
mViewPager.setOffscreenPageLimit(numOfTabs);
for ex:
mViewPager.setOffscreenPageLimit(10); //if you have 10 tabs
Hope this helps.

Can you have a loader without fragments

Very basic loader question.
My WIMMOne watch uses Android 2.1 (version 7).
There are no orientation changes, etc. with a watch. The small screen does not have room for any layout changes. So no need to deal with any kind of layout change.
The app I am working on now simply reads from a cursor, and displays an open ended scrolling list. My first app had a fragment and that was a pain. So I decided since I don't need fragments I will do away with the complexities of fragments.
I START WITH:
public class PhoneListActivity extends Activity
implements LoaderManager.LoaderCallbacks <Cursor>;
THEN:
protected void onCreate(Bundle savedInstanceState)
{ super.onCreate(savedInstanceState);
setContentView(R.layout.phone_list_activity);
FINALLY:
getLoaderManager().initLoader(0, null, this);
BUT:
Because it is 2.1 I need to use:
getSupportLoaderManager().initLoader(0, null, this);
BUT: -
That generates compile errors, so I need to use:
public class PhoneListActivity extends FragmentActivity . . . (not just Activity)
BUT: -
It immediately crashes on load in the ContentProvider.
Postings in various sites refer to "Activities and ActivityFragments".
SO: QUESTION 1: Can my main class use "extends FragmentActivity" without setting up a separate fragment (ie: leave it just as an Activity).
QUESTION 2: If not, does that mean that to use a loader I must set up a separate fragment and deal with the issues of fragments?
Many thanks,
Clark
The answer is YES.
I redid part of the program and it now works without using fragments. It has the open ended scrolling being loaded from a cursor and basically the same structure above
I think something in handling my cursor was causing it to crash.
Clark

Resources