In android, we keeps talking about retain the activity state/fragment state, but I have this question, what does "state" mean indeed. For example, suppose I have the following DialogFragment
public class Dialog extends DialogFragment {
private String mMessage;
#Override
public void onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
View v = super.onCreateView(inflater, container, savedInstanceState);
((TextView) v.findViewById("message")).setText(mMessage);
}
}
But wouldn't "mMessage" be retained as a member variable during device rotation? So in this case, does "mMessage" considered a state that I have to retain and put into argument when creating this fragment?
On a device rotation, the currently visible Activity is destroyed. Some widgets such as DialogFragment save and restore their own state.
Handling Configuration
Activity Lifecycle
The concept of State comes from OOP, not from android, to simplify: an object has a state (the data) and a behavior (the code).
Fragments and Activities work a bit different, fragments will retain the state if they are stopped, but they will lose it of the activity that manages them is destroyed (unless you retain it). Activities, however, will lose the state upon change of configuration.
The doc explains the lifecycle and how/when to retain fragments:
https://developer.android.com/guide/components/fragments.html#Lifecycle
Related
I'm kind of new in this Android Architecture, and I'm trying to understand the use of ViewModel.
Currently I have an activity with a fragment embedded in its xml. The fragment just have an edittext whose content I want to persist when the phone rotates. I've implemented the Viewmodel, livedata and observers correctly (I suppose), and attached the ViewModel to the fragment.
The problem comes here, when I rotate the phone the fragment is recreated and the information, gone. But, if I attached the ViewModel to the activity the app works as intended.
So, after some background info, the question is, Why would I want to attach the ViewModel to a fragment if the Viewmodel object is cleared when the fragment is recreated/destroyed?
Thanks
Ps. Here is how I attached the Viewmodel to the activity, this way it works as intended
Fragment
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...
final MuestrasViewModelFactory mvmFactory = new MuestrasViewModelFactory(new String[]{"hello","world","again"});
final MuestrasViewModel mvm = new ViewModelProvider(requireActivity(), mvFactory).get(MuestrasViewModel.class);
... //here comes the observers
My web app works great in Chrome which handles configuration changes (such as screen rotation) excellent. Everything is perfectly preserved.
When loading my web app into a WebView in my Android app then the web app loses state on screen orientation change. It does partially preserve the state, i.e. it preserves the data of the <input> form elements, however all JavaScript variables and DOM manipulation gets lost.
I would like my WebView to behave the way Chrome does, i.e. fully preserving the state including any JavaScript variables. It should be noted that while Chrome and WebView derives from the same code base Chrome does not internally use WebView.
What happens on screen orientation change is that the Activity (and any eventual Fragments) gets destroyed then subsequently recreated. WebView inherits from View and overrides the methods onSaveInstanceState and onRestoreInstanceState for handling configuration changes hence it automatically saves and restores the contents of any HTML form elements as well as the back/forward navigation history state. However the state of the JavaScript variables and the DOM is not saved and restored.
Proposed solutions
There have been a few proposed solutions. All of them non-working, only preserving partial state or in other ways suboptimal.
Assigning the WebView an id
WebView inherits from View which had the method setId which can also be declared in the layout XML file using the android:id attribute in the declaration of the <WebView> element. This is necessary for the state to be saved and restored, however the state is only partially restored. This restores form input elements but not JavaScript variables and the state of the DOM.
onRetainNonConfigurationInstance and getLastNonConfigurationInstance
onRetainNonConfigurationInstance and getLastNonConfigurationInstance are deprecated since API level 13.
Forcing screen orientation
An Activity can have its screen orientation forced by setting the screenOrientation attribute for the <Activity> element in the AndroidManifest.xml file or via the setRequestedOrientation method. This is undesired as it breaks the expectation of screen rotation. This also only deals with the change of screen orientation and not other configuration changes.
Retaining the fragment instance
Does not work. Calling the method setRetainInstance on a fragment does retain the fragment (it does not get destroyed), hence all the instance variables of the fragment are preserved, however it does destroy the fragment's view hence the WebView does gets destroyed.
Manually handling configuration changes
The configChanges attribute can be declared for an Activity in the AndroidManifest.xml file as android:configChanges="orientation|screenSize" to handle configuration changes by preventing them. This works, it prevents the activity from getting destroyed hence the WebView and its contents is fully preserved. However this has been discouraged and is said to be used only as a last resort solution as it may cause the app to break in subtle ways and get buggy. The method onConfigurationChanged gets called when the configChanges attribute is set.
MutableContextWrapper
I heard MutableContextWrapper can be used, but I haven't evaluated this approach.
saveState() and restoreState()
WebView have the methods saveState and restoreState. Note according to the documentation the saveState method no longer stores the display data for the WebView whatever that means. Either way these methods do not seem to fully preserve the state of the WebView.
WebViewFragment
The WebViewFragment is just a convenience fragment that wraps WebView for you so can easily get going with less boilerplate code, much like the ListFragment. It does not do any additional state preserving to fully preserve the state.
This class was deprecated in API level 28.
Question
Is there any real solution to the problem of WebView getting destroyed and losing its state upon configuration changes? (such as screen rotation)
A solution that fully preserves all the state including JavaScript variables and DOM manipulation. A solution that is clean and not built on hacks or deprecated methods.
After researching and trying out different approaches I have discovered what I have come to believe is the optimal solution.
It uses setRetainInstance to retain the fragment instance along with addView and removeView in the onCreateView and onDestroyView methods to prevent the WebView from getting destroyed.
MainActivity.java
public class MainActivity extends Activity {
private static final String TAG_FRAGMENT = "webView";
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebViewFragment fragment = (WebViewFragment) getFragmentManager().findFragmentByTag(TAG_FRAGMENT);
if (fragment == null) {
fragment = new WebViewFragment();
}
getFragmentManager().beginTransaction().replace(android.R.id.content, fragment, TAG_FRAGMENT).commit();
}
}
WebViewFragment.java
public class WebViewFragment extends Fragment {
private WebView mWebView;
public WebViewFragment() {
setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_webview, container, false);
LinearLayout layout = (LinearLayout)v.findViewById(R.id.linearLayout);
if (mWebView == null) {
mWebView = new WebView(getActivity());
setupWebView();
}
layout.removeAllViews();
layout.addView(mWebView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return v;
}
#Override
public void onDestroyView() {
if (getRetainInstance() && mWebView.getParent() instanceof ViewGroup) {
((ViewGroup) mWebView.getParent()).removeView(mWebView);
}
super.onDestroyView();
}
private void setupWebView() {
mWebView.loadUrl("https:///www.example.com/");
}
}
I would suggest you re-render the whole thing again. I searched a while and I couldn't find a clean ready made solution. Everything out there states that you let the webpage re-render on orientation change.
But if you really need to persist your JS variables, you could imitate what saveState and restoreState did. What these methods ideally do is save and restore stuff in WebView using the activity's onSaveInstanceState() and onRestoreInstanceState() respectively. These methods don't do the stuff they did because of potential memory leaks.
So, all you need to do is create your own webview (MyWebView extends WebView). In this have two methods: saveVariableState() and restoreVariableState(). In your saveVariableState() just save every variable you want in a bundle and return it ( public Bundle saveVariableState(){}). Now in the onSaveInstanceState() of the Activity, call MyWebView.saveVariableState and save the bundle it returns. Once the orientation changes, you fetch the bundle from onRestoreInstanceState or onCreate and pass it to the MyWebView via the constructor or restoreVariableState.
This is not a hack, but the normal way to save stuff of data's of other views. In case of WebView, instead of saving data of the view, you are going to save JS variables.
the following manifest code declares an activity that handles both the screen orientation change and keyboard availability change:
this code:
<activity android:name=".MyActivity">
becomes :
<activity android:name=".MyActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="#string/app_name">
here you only add: screenSize, then it works fine.
reference: https://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject
Simple thing you could do is something like:
WebView webView;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_webview, container, false);
webView = v.findViewById(R.id.webView);
if(savedInstanceState != null){
webView.restoreState(savedInstanceState);
} else {
loadUrl();
}
return v;
}
private void loadUrl(){
webView.loadUrl("someUrlYouWant");
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
webView.saveState(outState);
}
NOTE: Did not try this code just wrote it but think it should work.
This confuses me a lot.
I had two fragments, and I use FragmentTransaction.replace() the swap the two fragments. State of some input controls like RadioGroup and checkbox is always preserved when I switch to the other fragments and switch back again.
.
I set break point on my OnCreateView() of Fragment, and every swap seems to trigger the method and new views are created each time. The savedInstanceBundle is always null as well. Since the new views are created, how do they get the state of the old views?
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_fieldsetup_marking, container, false);
// ...
return v;
}
This effect is fine for my app, but I just want to know how it is implemented.
where are the states stored?
Where is the android SDK code to restore this (I can not find any in the class FragmentTransection).
In an Android Fragment, which is a part of a ViewPager, there is a ListView with EditText for filtering.
filterEditText = (EditText) view.findViewById(R.id.filter_friends);
filterEditText. addTextChangedListener(textWatcher);
When I navigate to another Fragment ondestroyview is called and then when I navigate back to this fragment onCreateView is called and the filtering doesn't work anymore, though the instance variables still exist.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_new_game_facebook, container,
false);
return view;
}
How this situation should be handled correctly?
You are probably using a FragmentStatePagerAdapter, which will destroy all non-active fragments to save memory. This is helpful for when you do not know how many fragments you will use until runtime, but is overly aggressive if you know which fragments will be in the pager. Try using a FragmentPagerAdapter instead, which will not destroy the fragments as soon as you navigate away from them, so you will not need to manually persist the state of each fragment and reload it.
I am trying to save my View states in my fragment but I am concerned I make be leaking my Activity. Here is what I am doing:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state){
if(mView != null){
View oldParent = mView.getParent();
if(oldParent != container){
((ViewGroup)oldParent).removeView(mView);
}
return mView;
}
else{
mView = inflater.inflate(R.id.fragview, null)
return mView;
}
}
I am concerned because I know all Views hold onto a context and I don't know if it is the Activity context or Application context if inflated from the inflater. Perhaps it would be a better idea to pragmatically create the view and set its attributes using getActivity().getApplication() rather than use the inflater. I would appreciate any feedback on this.
Thanks!
EDIT: Confirmed Activity leak, although this code works great don't do it :*(
I am trying to save my View states in my fragment but I am concerned I make be leaking my Activity.
Use onSaveInstanceState() (in the fragment) and onRetainConfigurationInstance() (in the activity) for maintaining "View states" across configuration changes. I am not quite certain what other "View states" you might be referring to.
I am concerned because I know all Views hold onto a context and I don't know if it is the Activity context or Application context if inflated from the inflater.
Since using the Application for view inflation does not seem to work well, you should be inflating from the Activity. And, hence, the views will hold a reference to the Activity that inflated them.
#schwiz é have implemented something similar.
I use the setRetainInstance in the fragment. on the fragment layout I have a FrameLayout placeholder where I put my webView inside.
This webView is created programmatically on the Fragment's onCreate using the Application Context, and I do a addView(webView) on the inflated layout in onCreateView().
webView = new WebView(getActivity().getApplicationContext());
And on onDestroyView I simply remove the webView from my framelayout placeholder.
It work really well, unless you try to play a video in fullscreen. That doesn't work because it expects an Activity Context
I am trying to save my View states in my fragment
In order to save and retain view's state you can just use View.onSaveInstanceState () and View.onRestoreInstanceState (Parcelable state)
It will help you to handle saving and restoring view state independantly from neither activity or fragment.
See this answer for more information about it (how to prevent custom views from losing state across screen orientation changes)