The MVP pattern. Who are you, WebView-View or Model?

Hello everyone, I'm building an MVP architecture in my project and I have problems in WebView. Look carefully at this class.

public class LoginWebView extends WebView
{
    public LoginWebView(Context ctx)
    {
        this(ctx, null);
    }

    public LoginWebView(Context ctx, AttributeSet attrs)
    {
        super(ctx, attrs);
        getSettings().setDomStorageEnabled(true);
        getSettings().setJavaScriptEnabled(true);
        getSettings().setLoadsImagesAutomatically(false);
    }

    public void authorize(LoginCallback callback, GameProvider provider, String login, String password)
    {
        setWebViewClient(new LoginWebViewClient(callback, provider, login, password));
        setWebChromeClient(new WebChromeClient());
        loadUrl(provider.getLoginPage());
    }

    class LoginWebViewClient extends WebViewClient
    {

        private LoginCallback callback;
        private GameProvider provider;
        private String login, password;
        private boolean jsInjected;

        public LoginWebViewClient(LoginCallback callback, GameProvider provider, String login, String password)
        {
            this.callback = callback;
            this.provider = provider;
            this.login = login;
            this.password = password;
        }

        @Override
        public void onPageFinished(WebView view, String url)
        {
            super.onPageFinished(view, url);
            if (!jsInjected)
            {
                jsInjected = true;
                Toast.makeText(getContext(), "Login script...", 1).show();
                loadUrl(provider.getLoginScript(login, password));
            }
            else
            {
                setWebViewClient(new WebViewClient());
                setWebChromeClient(new LoginWebChromeClient(callback, provider));
                loadUrl("javascript:(function() { alert(document.documentElement.outerHTML); })()");
            }
        }

        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
        {
            setWebViewClient(new WebViewClient());
            callback.onError("Ошибка подключения");
        }

    }

    class LoginWebChromeClient extends WebChromeClient
    {
        private LoginCallback callback;
        private GameProvider provider;

        public LoginWebChromeClient(LoginCallback callback, GameProvider provider)
        {
            this.callback = callback;
            this.provider = provider;
        }

        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult result)
        {
            setWebChromeClient(new WebChromeClient());
            if (provider.loginIsSuccess(Jsoup.parse(message)))
            {
                callback.onSuccess();
            }
            else
            {
                callback.onError("Неверный логин илм пароль");
            }
            return true;
        }
    }
}

In my case, WebView is needed for hidden authorization (something like headless), since there are not enough parser capabilities. If there was no need for JS, then WebView could be thrown out, but unfortunately Android doesn't provide anything, even like that, in return.

On the one hand, yes, WebView is a View (logical, right?). But View-display, that is, what we see. In my case, it is hidden from the user and is used simply because there are no other tools. So I put it in the data layer. This is a platform-specific repository.

But, unfortunately, it is not possible to create WebView programmatically (and if it is possible, then the context does not belong in the data layer). This means that the option of inheritance is no longer available. If you replace inheritance with composition and pass WebView through some setter, then this means that the View (it's in the markup!) must go through all the layers.

Both options break the MVP pattern, but are inherently logical. However, what should I do?

Author: Tony, 2018-09-18

2 answers

WebView is a view that is placed on the Activity and in theory, this Activity is the view in the mvp pattern

 1
Author: Tony, 2018-09-18 12:38:39

It turned out something like this:

class HeadLessLoginHelper(private val loginData: LoginData,
                          private val provider: GameProvider,
                          private val loginCallback: LoginCallback) : WebViewClient() {

    private var loginScriptInjected = false

    override fun onPageFinished(webView: WebView, url: String?) {
        super.onPageFinished(webView, url)
        if (!loginScriptInjected) {
            loginScriptInjected = true
            webView.runJavascript(readAsset(provider.loginScriptPath), loginData.login, loginData.password)
        } else {
            webView.webViewClient = WebViewClient()
            webView.webChromeClient = JsAlertInterceptor()
            webView.runJavascript("function() { alert(document.documentElement.outerHTML); }")
        }
    }

    @Suppress("OverridingDeprecatedMember")
    override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
        webView.loadUrl(url)
        return true
    }

    @TargetApi(Build.VERSION_CODES.N)
    override fun shouldOverrideUrlLoading(webView: WebView, request: WebResourceRequest): Boolean {
        webView.loadUrl(request.url.toString())
        return true
    }

    override fun onReceivedError(webView: WebView, request: WebResourceRequest?,
                                 error: WebResourceError?) {
        webView.webViewClient = WebViewClient()
        loginCallback.onError(ConnectException())
    }

    inner class JsAlertInterceptor : WebChromeClient() {

        override fun onJsAlert(webView: WebView, url: String,
                               message: String, result: JsResult?): Boolean {
            webView.webChromeClient = WebChromeClient()
            val host = Uri.parse(url).host
            val afterHost = url.split(host)[1]
            try {
                provider.checkLoginResult(host, afterHost, Jsoup.parse(message))
                loginCallback.onSuccess()
            } catch (ex: Exception) {
                loginCallback.onError(ex)
            }
            return false
        }
    }

    companion object {
        fun login(webView: WebView, loginData: LoginData,
                  gameProvider: GameProvider, onSuccess: () -> Unit, onError: (Exception) -> Unit) {
            val loginCallback = LoginCallback(onSuccess, onError)
            webView.webViewClient = HeadLessLoginHelper(loginData, gameProvider, loginCallback)
            webView.loadUrl(gameProvider.loginUrl)
        }
    }

}
 1
Author: , 2018-09-19 13:46:07