Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 880
Fred Avatar asked Oct 30 '15 23:10

Fred


1 Answers

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/");
    }
}
like image 126
Fred Avatar answered Oct 09 '22 22:10

Fred