-
Notifications
You must be signed in to change notification settings - Fork 7
Quick Start
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
.
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:
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
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
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.
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.