Skip to content

Quick Start

Artur edited this page Sep 3, 2017 · 1 revision

Let's make a very basic authorization screen. We need Activity with two input fields for login and password, a sign in button, TextView for displaying the result and a ProgressBar.

public class LoginActivity extends AppCompatActivity {

    private EditText loginInput; // login field
    private EditText passwordInput; // password field
    private View loginActionView; // sign in button
    private View progressView; // progress indicator
    private View successView; // a widget we'll show upon a successful sign in

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        loginInput = (EditText) findViewById(R.id.login);
        passwordInput = (EditText) findViewById(R.id.password);
        loginActionView = findViewById(R.id.login_action);
        progressView = findViewById(R.id.progress);
        successView = findViewById(R.id.login_success);
}

The next step, we’ll make this screen more Reamp.

First, create a state class for this screen

public class LoginState extends SerializableStateModel {
    public String login;
    public String password;
    public boolean showProgress;
    public Boolean loggedIn;

    public boolean isSuccessLogin() {
        return loggedIn != null && loggedIn;
    }
}

In the String login and String password fields we will store the login and password the user has entered. boolean showProgress flag will mark the presence of an active logon request. Boolean loggedIn field will be considered the sign in result: null – no logon attempt yet, true or false – successful or unsuccessful logon.

Now, let's create a presenter class for this screen

public class LoginPresenter extends MvpPresenter<LoginState> {

    @Override
    public void onPresenterCreated() {
        super.onPresenterCreated();
        // setting up a display upon a fresh start
        getStateModel().setLogin("");
        getStateModel().setPassword("");
        getStateModel().setLoggedIn(null);
        getStateModel().setShowProgress(false);
        sendStateModel(); //submitting LoginState for rendering
    }

    // called by the View class when a logon should be executed
    public void login() {
        
        getStateModel().setShowProgress(true); // the screen should display the progress
        getStateModel().setLoggedIn(null); // the logon attempt result unknown yet
        sendStateModel(); // submitting the current screen state for rendering
        
        // emulating five second long sign in request
        new Handler()
                .postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        getStateModel().setLoggedIn(true); // successful logon notification
                        getStateModel().setShowProgress(false); // removing the progress indicator
                        sendStateModel(); // submitting the current screen state for rendering
                    }
                }, 5000);
    }

    public void loginChanged(String login) {
        getStateModel().setLogin(login); // saving the user’s input data
    }

    public void passwordChanged(String password) {
        getStateModel().setPassword(password); // saving the user’s input data
    }
}

Presenter and the model for the screen are ready, now tuning Activity itself is remaining (duplicated code is left out)

public class LoginActivity extends MvpAppCompatActivity<LoginPresenter, LoginState> {

     /***/

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        
        /***/

        loginActionView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getPresenter().login(); // notifying the presenter of the event
            }
        });

        // monitoring the user’s input
        loginInput.addTextChangedListener(new SimpleTextWatcher() {
            @Override
            public void afterTextChanged(Editable s) {
                getPresenter().loginChanged(s.toString()); // notifying the presenter of the event
            }
        });

        // monitoring the user’s input
        passwordInput.addTextChangedListener(new SimpleTextWatcher() {
            @Override
            public void afterTextChanged(Editable s) {
                getPresenter().passwordChanged(s.toString()); // notifying the presenter of the event
            }
        });
    }

    // called by Reamp when a fresh LoginState model is to be created
    @Override
    public LoginState onCreateStateModel() {
        return new LoginState();
    }

    // called by Reamp when a fresh LoginPresenter is to be created
    @Override
    public MvpPresenter<LoginState> onCreatePresenter() {
        return new LoginPresenter(loginService);
    }

    // called by Reamp every time the screen state changes
    @Override
    public void onStateChanged(LoginState stateModel) {
        progressView.setVisibility(stateModel.showProgress() ? View.VISIBLE : View.GONE); // setting the desired state of the progress indicator
        loginActionView.setEnabled(!stateModel.showProgress()); // while the query is in process, the logon button is disabled
        successView.setVisibility(stateModel.showSuccessLogin() ? View.VISIBLE : View.GONE); // setting the desired state of the ‘successful’ widget
    }

    static class SimpleTextWatcher implements TextWatcher {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {}

        @Override
        public void afterTextChanged(Editable s) {}
    }
}

All set! Let's sum up what is going on here.

We've created a LoginState, which inherits the SerializableStateModel class. In the LoginState we store all the data Activity is supposed to display to the user on the screen. Aside from this, we've created a LoginPresenter, which uses the LoginState model to inform its Activity what information it needs to display. Our LoginActivity inherits the MvpAppCompatActivity class provided by the Reamp library (if desired, your own class that implements the MvpView interface can be written). Every time the LoginActivity receives a message that the data in LoginState has changed, it redraws its content according to the new LoginState.

What have we gained with it?

If one looks at the code, it may seem that all we've done is transferring the significant dynamic data to LoginState, transferring part of the code (such as the logon request) from the Activity to the Presenter, and that's all. Actually, it is :) But thanks to the invisible work of Reamp, in this example we already have:

System configuration change processing (in particular, the screen rotation)

When rotating the screen, the five-second logon operation does not interrupt, does not restart; it will proceed till the end. Try to rotate the screen and you'll see that after the rotation our LoginActivity is still displaying the progress indicator; and after the logon has been executed, it will show the corresponding message. Yet you do understand this is another LoginActivity, right? ;) If the logon process ends exactly at the moment when the first LoginActivity has already terminated, and the new 'LoginActivity` has not appeared yet, then nothing bad will occur. The logon result will be saved in the 'LoginState', and a new Activity will immediately receive this result.

Note that we haven't written any code for saving and restoring a screen state, and our Activity doesn't contain the android:configChanges cheat flag in the manifest. If we use Fragment as MvpView, everything will work without the setRetainInstance(true) flag

System resource cleaning processing

The previous point may lead to a guess that the logon process will work out even if the user minimizes the app to mind their own business. A phone call, a game, watching a video – any resource-intensive activity may cause the system to unload our screen from memory. When the user finishes all their important endeavors and returns to the app, they will see the logon result, not an empty logon screen, as it often happens

Rendering exception processing

Exceptions in Java are a powerful mechanism that requires some skill to use. Errors occurring at the level of data transfer/manipulation should be checked and processed at the level of business logic. Alas, at the UI level these errors are more of a pesky imbroglio rather than an architectural miscalculation. That is why the calls to the MvpView.onStateChanged(...) method are protected from unprocessed exceptions. We consider it better if a hypothetical social network app user doesn't see the comment date on the screen if it could not be converted from the string rather than gets the crash of the whole app.

Delegating part of UI logic to StateModel

It often happens that an interface element state depends on one or more parameters. StateModel is great for arranging such parameters and calculating the state. Say, we'd like the logon button to be active if:

  • user has filled the login field
  • user has filled the password field
  • user hasn’t logged in yet
  • logon query isn’t in progress

Such a task me be completed using a LoginState modification

public class LoginState extends SerializableStateModel {

   /***/

    public boolean isLoginActionEnabled() {
        return !showProgress
                && (loggedIn == null || !loggedIn)
                && !TextUtils.isEmpty(login)
                && !TextUtils.isEmpty(password);
    }
}
//LoginActivity
public void onStateChanged(LoginState stateModel) {
        /**/
        loginActionView.setEnabled(stateModel.isLoginActionEnabled());
}

Now, upon every change of LoginState, the logon button state will always remain relevant.