Zastosowanie
MVP
(Model-View-Presenter
) usprawnia proces tworzenia ekranów aplikacji poprzez podział odpowiedzialności na trzy rozdzielne warstwy: widoku (View
), prezentera (Presenter
) oraz modelu (Model
). Warstwa widoku
odpowiedzialna jest za przechwytywanie interakcji użytkownika i odsyłanie zdarzeń do prezentera, a także za sposób prezentowania danych oraz stanu systemu i wykonywanych operacji w interfejsie graficznym. Warstwa prezentera
obsługuje żądania podjęcia akcji na rzecz widoku poprzez proces sprawdzania stanu systemu, walidacji danych, tworzenia zapytań do modelu, a także informowanie widoku o aktualnym stanie zadania. Warstwa modelu
zajmuje się logiką biznesową, przetwarzaniem, przechowywaniem oraz dostarczaniem żądanych danych do prezentera. Dzięki zastosowanemu podziałowi odpowiedzialności kod staje się uporządkowany, czytelny, otwarty na modyfikacje, łatwiejszy do debugowania, przeprowadzenia testów oraz ułatwia zespołom jednoczesną pracę nad jednym ekranem. MVP
jest przede wszystkim konceptem, nie zbiorem sztywnych reguł implementacji.
Ograniczenia
Ze względu na cykl życia Aktywności
, Fragmentu
czy innych komponentów interfejsu użytkownika może pojawić się wyciek pamięci referencji do nieistniejącego już widoku w prezenterze. Należy zatem zadbać o odpowiednią obsługę metod cyklu życia. Co więcej odtworzenie stanu prezentera (np. po obrocie ekranu) jest mocno utrudnione. Pomimo zalet płynących z abstrakcji, implementacja wzorca MVP
wymaga stworzenia wielu dodatkowych klas i interfejsów (nierzadko o podobnej zawartości) co wiążę się często z nadmiarowym kodem. Ponadto istnieje zagrożenie, że klasa prezentera może stać się superklasą
. Jako alternatywę warto rozważyć zastosowanie wzorca MVVM
.
Użycie
Wzorzec MVP
wykorzystywany jest przede wszystkim w celu zlikwidowania problemu God Activity
, czyli Aktywności
posiadającej zbyt dużą odpowiedzialność i dużo kodu. Dzięki separacji zadań w różnych warstwach wzorzec może być używany, aby zwiększyć czytelność kodu i możliwość niezależnego testowania logiki biznesowej oraz przepływu interakcji (warstwa prezentera jest niezależna od klas Androidowych
).
Implementacja
Zgodnie z podstawowymi założeniami wzorca warstwa widoku ViewImpl
implementuje interfejs View
oraz inicjalizuje instancje prezentera do którego deleguje wykonanie zadań. Klasa PresenterImpl
jest dedykowana dla klasy widoku przez co sama w sobie jest swego rodzaju abstrakcją, dlatego nie ma wymogu, aby dodatkowo implementowała ona interfejs. Klasa PresenterImpl
posiada referencje do widoku i modelu dla których pełni rolę pośrednika w komunikacji. Warstwa prezentera
jest wolna od zależności klas Androidowych. Klasa Model
dostarcza implementację zachowania modyfikacji i pobierania danych.
Poniższy listing przedstawia realizacje wzorca MVP
spełniającą minimalne założenia implementacji wraz podstawowym podziałem na warstwy. Rozpoczęcie działania następuje w widoku.
public class ViewImpl implements View {
//this class is in charge and initialize everything so in Android it could be Activity
//for simplify MVP idea any Android class is used
//some view fields like buttons, textview etc
private TextView textView;
private EditText editText;
private Button button;
private PresenterImpl presenter;
//for Activity, Fragment, Custom view or other View layer
//initialize view and presenter in lifecycle methods
public ViewImpl() {
initView();
initPresenter();
initListeners();
}
private void initView() {
//set default text/image etc in the views
}
private void initListeners() {
button.setOnClickListener(v -> {
String input = editText.getText().toString();
presenter.onButtonClicked(input)
});
}
private void initPresenter() {
//in some variants Model object layer could be injected to presenter also
this.presenter = new PresenterImpl(this);
}
@Override
public void updateText(String text) {
textView.setText(text);
//show response text in the textView
}
@Override
public void showError(String error) {
//show error on the screen (in Android like Toast)
}
@Override
public void showProgress(boolean enable) {
if(enable) {
//show progress bar
}
else {
//hide progress bar
}
}
}
public class PresenterImpl {
private View view;
private Model model;
public Presenter(View view) {
this.view = view;
this.model = model; //can be injected by the View
}
@Override
public void onButtonClicked(String input) {
//do proper action
view.showProgress(true);
String response = model.getDescription(input);
if(response.equals("EMPTY")) {
view.showProgress(false);
view.showError();
}
else {
view.showProgress(false);
view.updateText(response);
}
}
}
public class Model {
private Database database;
//other variables, logic managers or data providers like database, cache, network
public Model() {
//do init staff
//initialize some instances
database = new Database();
}
public String getDescription(String input) {
String result = database.get("WORD", input);
if(result.equals(""))
return "EMPTY";
else
return result;
}
}
interface View {
void updateText(String text);
void showError(String error);
void showProgress(boolean enable);
}
Istnieje wiele wariantów rozszerzenia podstawowej implementacji wzorca MVP
, a różnice między nimi są obiektem żywych dyskusji. Popularną praktyką jest tworzenie kontraktu Contract
w postaci interfejsów View
i Presenter
co jasno definiuje oczekiwane zachowania między warstwą widoku i prezentera
, umożliwia mockowanie prezentera, a także chroni strukturę klas przed niepowołanym dostępem. Czerpiąc inspiracje ze wzorca VIPER
warstwa modelu
jest często realizowana w postaci klasy InteractorImpl
, która implementuje interfejs Interactor
, a wyniki operacji zwraca do instancji typu OnResult
(implementowanego przez PresenterImpl
). Dzięki temu warstwa modelu przejmuje część odpowiedzialności warstwy prezentera, która od tego momentu zajmuje się tylko przechwytywaniem zdarzeń z interfejsu użytkownika oraz przygotowaniem danych (pochodzących z interaktora) dla widoku. Aby odciążyć warstwę widoku
od odpowiedzialności nawigowania między modułami można wykorzystać w tym celu pomocniczą klasę Router
. Gdy w warstwie modelu zachodzi potrzeba wykorzystania obiektu typu Context
wówczas dostęp do niego może być realizowany poprzez wstrzykiwanie zależności
z poziomu warstwy widoku lub dostarczany ze statycznego kontekstu aplikacji. Ponadto nie należy wiązać prezentera z metodami cyklu życia konkretnych komponentów, a co najwyżej ograniczyć się do metod initialize
oraz uninitialize
.
Przykład
Ekran logowania ViewActivity
w procesie autoryzacji użytkownika wykorzystuje komunikacje sieciową. Zapamiętuje ostatnio poprawnie zalogowanego użytkownika i automatycznie uzupełnia pole loginu. Do implementacji tego procesu wykorzystano wzorzec MVP
w wariancie Contract View-Presenter
wraz z Interactor
jako warstwa modelu.
public class ViewActivity extends AppCompatActivity implements View {
private EditText loginEdit;
private EditText passwordEdit;
private Button loginButton;
private Presenter presenter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.view_activity);
presenter = new PresenterImpl(this);
initView();
setListeners();
setViews();
}
//other lifecycle methods
private void initView() {
loginEdit = findViewById(R.id.loginEdit);
passwordEdit = findViewById(R.id.passwordEdit);
loginButton = findViewById(R.id.loginButton);
}
private void setListeners() {
loginButton.setOnClickListener(v -> { loginAction(); });
}
private void setViews() {
presenter.initView();
}
@Override
public void setLastLogin(String login) {
loginEdit.setText(login);
}
@Override
public void navigateToHome() {
//go to another activity (like home screen)
//instead of doing logic operations it can be delegate to Router helper class
}
@Override
public void showError() {
Toast.makeText(this, "Incorrect login or password", Toast.LENGTH_LONG).show();
}
@Override
public void showInvalidLoginError(boolean enable) {
if(enable) {
//show invalid login error
}
else {
//clear error
}
}
@Override
public void showInvalidPasswordError(boolean enable) {
if(enable) {
//show invalid password error
}
else {
//clear error
}
}
private void loginAction() {
//controller get inputs from view
String login = loginEdit.getText().toString();
String password = passwordEdit.getText().toString();
presenter.loginButtonClicked(login, password);
}
}
public class PresenterImpl implements Presenter, OnResult {
private View view;
private Interactor interactor;
public PresenterImpl(View view) {
this.view = view;
this.interactor = new InteractorImpl(this); //can be injected by the View
}
@Override
public void loginButtonClicked(String login, String password) {
//validate login and password based on rules
if(isLoginValid(login)) {
view.showInvalidLoginError(false);
if(isPasswordValid(password)) {
view.showInvalidPasswordError(false);
interactor.login(login, password);
}
else
view.showInvalidPasswordError(true);
}
else
view.showValidLoginError(true);
}
@Override
public void initView() {
interactor.getSavedLogin();
}
@Override
public void onLoginSuccess() {
view.navigateToHome();
}
@Override
public void onLoginFail() {
view.showError();
}
@Override
public void onSavedLoginExists(String savedLogin) {
view.setLastLogin(savedLogin);
}
private boolean isLoginValid(String login) {
//check rules e.g. no special characters
return true; //mock
}
private boolean isPasswordValid(String password) {
//check rules e.g. length
return true; //mock
}
}
public class InteractorImpl implements Interactor {
//some network provider
private SharedPreferences sharedPref;
private SharedPreferences.Editor editor;
private Context context;
private OnResult listener;
public InteractorImpl(OnResult listener) {
this.listener = listener;
this.context = App.getContext();
sharedPref = context.getPreferences(Context.MODE_PRIVATE);
}
@Override
public void login(String login, String password) {
//use some network library to login
int code = Response.OK; //mock code response from network
if(code == Response.OK) {
saveLogin(login);
listener.onLoginSuccess();
}
else {
listener.onLoginFail();
}
}
@Override
public void getSavedLogin() {
String savedLogin = sharedPref.getString("login", "");
if(!savedLogin.equals(""))
listener.onSavedLoginExists(savedLogin);
}
private void saveLogin(String login) {
editor = sharedPref.edit();
editor.putString("login", login);
editor.commit();
}
}
interface Contract {
interface View {
void setLastLogin(String login);
void navigateToHome();
void showError();
void showInvalidLogin(boolean valid);
void showInvalidPassword(boolean valid);
}
interface Presenter {
void initView();
void loginButtonClicked(String login, String password);
}
}
interface Interactor {
void getSavedLogin();
void login(String login, String password);
interface OnResult {
void onLoginSuccess();
void onLoginFail();
void onSavedLoginExists(String savedLogin);
}
}
Biblioteki
Biblioteka Mosby
oraz ThirtyInch
są przykładem realizacji wzorca MVP
. Zmniejszają ilość tworzonego ręcznie kodu oraz automatyzują przywracanie stanu prezentera. Programista implementujący wzorzec MVP
powinien pamiętać przede wszystkim o niezależności warstwy prezentera od klas Androidowych, a także o dostosowaniu wariantu rozszerzenia wzorca do realizowanych potrzeb.