Fred
Fred

Reputation: 12776

Handling screen rotation in WebView

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.

Upvotes: 14

Views: 5493

Answers (4)

user1540907
user1540907

Reputation: 201

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.

Upvotes: 1

aslam parvaiz
aslam parvaiz

Reputation: 87

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

Upvotes: 1

Fred
Fred

Reputation: 12776

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/");
    }
}

Upvotes: 9

Henry
Henry

Reputation: 17841

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.

Upvotes: 0

Related Questions