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.
Related
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
if the question sounds weird at first, here comes the explanation:
I have got an activity that hosts my three fragments. Since I would like one of my fragments to save its instance state when the device is rotated, I defined this in my manifest for my activity that hosts the fragments:
android:configChanges="orientation|screenSize"
This works just fine. However, now I have got an other problem: One of my other fragments uses a special landscape layout. The problem is, that this layout is not used immediately on device rotation. I think it is because the new layout only gets set on onCreate.
What can I do to solve this problem? I want my landscape layout to be set immediately.
You can put
setRetainInstance(true);
in onCreateView(); method of your Fragment. I think it should do the trick.
As far as I know you down need to add the configChanges parameter to your manifest.
You can override onSaveInstanceState() in your Fragment
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putInt(KEY_INDEX, someIntValue);
}
This methode should be called before your fragment gets destroyed.
Now in your onCreate(Bundle savedInstanceState) (or onCreateView()) methode:
if (savedInstanceState != null) {
someIntValue = savedInstanceState.getInt(KEY_INDEX);
}
This way it shouldn't intervene with any other special fragments.
My app have two different layout for portrait and landscape mode which both encapsulate a webview and some buttons. Buttons are at bottom in portrait and at left in landscape so more reading space is available in webview.
The problem is, the activity is recreated on screen rotation and webview loads the first page which is not a wanted behavior.
I searched and found out using android:configChanges="orientation" in activity tag prevents recreating of the activity. But the porblem is it prevents the layout changing too as it happens in activity creation.
I want my program to work in 2.2, waht's the best way to this?
I tested fragments, but dealing with fragment makes things much more complex and the fragment itself needs saving and restoring which may not work in a webview which has javascript state, So I searched more and find a nice article somewhere and with some modification I came to a solution which I suggest:
First, add android:configChanges="orientation|screenSize|keyboard|keyboardHidden" to manifest so app handles the config change instead of android.
Make two different layout for lnadscape and portrait mode and put them in corresponding layout folders. In both layouts instead of webview place a LinerLayout which acts as a placeholder for webview.
In code define initUI method like this and put every thing related to UI initialization in this method:
public void initui()
{
setContentView(R.layout.main);
if (wv == null) wv = new WebView(this);
((LinearLayout)findViewById(R.id.webviewplace)).addView(wv);
findViewById(R.id.home).setOnClickListener(this);
}
If the webview doesn't exist, it will be created and after setContentView(R.layout.main) it will be added to the layout. Other UI customization came afterward.
and in onConfigurationChanged:
#Override
public void onConfigurationChanged(Configuration newConfig)
{
((LinearLayout)findViewById(R.id.webviewplace)).removeAllViews();
super.onConfigurationChanged(newConfig);
initUI();
}
In onConfigChange First the webview is removed from old place holder and initui will be called which will add it back to the new layout.
and in oncreate call initui so the ui will be initialized for the first time.
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
initUI()
}
Could you not save the last loaded URL in the webview in onSaveInstanceState(Bundle outState) and then make sure to reload it onRestoreInstanceState(Bundle savedInstanceState) using myWebview.loadUrl(restoredUrl)?
edit I know that this might not work if the web page you are displaying requires a state to be kept. But if not it should be a solution to your problem.
From this document https://developer.android.com/guide/topics/resources/runtime-changes.html#HandlingTheChange
I just edited manifest with android:configchanges so it works fine for me.
<activity android:name=".Webhtml"
android:configChanges="orientation|screenSize"/>
I have a tabhost with 3 tabs. In each of these tabs there is a webiview.
When i click a tab the webview need to "reload" even when i have been there before, it have not been saved.
Is there any way to save the webview ?
This can be handled by overrwriting onSaveInstanceState(Bundle outState) in your activity and calling saveState from the webview:
#Override
protected void onSaveInstanceState(Bundle outState) {
webView.saveState(outState);
super.onSaveInstanceState(outState);
}
Then recover this in your onCreate after the webview has been re-inflated of course:
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.blah);
WebView webview = (WebView)findViewById(R.id.webview);
if (savedInstanceState != null)
webview.restoreState(savedInstanceState);
else
webview.loadUrl(URLData)
}
restoreState was never reliable. And come 2015 the documents accept the fact. (The change was probably made to the documents around 2014).
This is what it says now.
If it is called after this WebView has had a chance to build state
(load pages, create a back/forward list, etc.) there may be
undesirable side-effects. Please note that this method no longer
restores the display data for this WebView.
And the corresponding entry for saveState() speaks thus:
Please note that this method no longer stores the display data for
this WebView.
What you really should do inside the onCreate method is to call webView.loadUrl() if you want to display the last visited url, please see this answer:
If you are concerned about the webview reloading on orientation change etc.
you can set your Activity to handle the orientation and keyboardHidden changes, and then just leave the WebView alone
Alternatively you can look into using fragments with your tabHost. when entering another webView you can set fragment to hide and the other to show. when you return to the previous fragment the content will not have been destroyed. Thats how i did it and it worked for me.
How do I retain the state of an activity in android? I have two layouts for portrait and landscape in layout and layout-land. I am loading the value from service at the time I am showing progress dialog. If loaded user rotates the device to landscape at the time also loading. How do I avoid that? user typed content in webview that also refreshed. How do I avoid that, can anybody provide an example?
Thanks
When orientation changes, the Activity is reloaded by default. If you do not want this behavior then add this to the Activity definition in your manifest:
android:configChanges="orientation|keyboardHidden"
For more detail, see Handling Runtime Changes
You can use the onRetainNonConfigurationChange() callback to store arbitrary data. It is called just before your application is about to be recreated.
Then, in onCreate() just check if some data were put aside by calling getLastNonConfigurationInstance() that returns the Object you put aside or null.
See this article on android developers.
Here's a sample borrowed from the link above:
#Override
public Object onRetainNonConfigurationInstance() {
//this is called by the framework when needed
//Just return what you want to save here.
return MyBigObjectThatContainsEverythingIWantToSave;
}
Automagic restore of previously saved state:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final MyDataObject MyBigObjectThatContainsEverythingIWantToSave = (MyDataObject) getLastNonConfigurationInstance();
if (MyBigObjectThatContainsEverythingIWantToSave == null) {
//No saved state
MyBigObjectThatContainsEverythingIWantToSave = loadMyData();
} else {
//State was restored, no need to download again.
}
...
}
When orientation changes, the Activity is reloaded by default. If you do not want this behavior then add this to the Activity definition in your android manifest file :
android:configChanges="orientation|screenSize|keyboardHidden"