Ultimamente il mio team ha iniziato a considerare l'implementazione del pattern MVP in alcune delle nostre applicazioni.
Abbiamo seguito le diverse guide e tutorial là fuori, fondamentalmente finendo con regolare PresenterInterface
e ViewInterface
, quest'ultimo essendo attuato da un Activity
o Fragment
.
Come PresenterInterface
, abbiamo creato un'implementazione e l'abbiamo iniettata in Activity
con dagger.
Quindi PresenterImplementation
manterrà traccia dello stato ed eseguirà tutta la logica dietro il comportamento di Activity
.
Potremmo pubblicare con successo alcune applicazioni basate su questo modello e tutto sembra funzionare abbastanza bene.
Ultimamente pensavo, ho iniziato a mettere in discussione questo modo di implementare il pattern MVP come mi sembra che ci sia uno strato logico extra aggiunto tra loro senza essere menzionato:
- Non è
View
la vista attuale? - Voglio dire, si dispone di unActivity
e si utilizzasetContentView(...)
poi confindViewById(...)
si ottiene un'istanza della classeView
che rappresenta la vista reale analizzato da XML o di programmazione integrata .
- Quindi se
View
è la vista, non è ilActivity
il presentatore effettivo? - Voglio dire che il concettoActivity
è lì per rappresentare una logica dietro la vista. - Il presentatore dovrebbe mantenere lo stato: beh, esistono diversi modi per mantenere lo stato all'interno e
Activity
. Ad esempio,SavedInstanceState
,SharedPreferences
ecc. - Tutti gli% ascoltatori% co_de (ad esempio
View
) sono i metodi che la View può invocare il suo presentatore, mentre un composto CustomView può essere creato esporre metodi per impostare i valori dei suoi Sub-Vista con interfaccia.
Con il modello "normale" MVP abbiamo usato fino ad ora, mi sembra che la OnClickListener
è lì solo per accedere ai suoi Activity
s 'sub-viste e comandi 'porta' del presentatore davanti a loro.
Quindi, nella mia idea, avresti qualcosa di simile a questo:
Struttura
- main
|
|- MainActivity (class)
|- MainPresenter (interface)
|- MainView (interface)
|- MainViewImpl (class)
MainView
public interface MainView {
void setTitle(String title);
void setBackgroundColor(int backgroundColor);
}
MainPresenter
public interface MainPresenter {
void awkwardButtonPressed();
void bind(MainView mainView);
void setup();
}
MainViewImpl
public class MainViewImpl extends FrameLayout implements MainView{
LayoutInflater inflater;
TextView tvTitle;
Button btnAwkward;
MainPresenter mainPresenter;
public MainViewImpl(Context context) {
super(context);
if(!(context instanceof MainPresenter))
throw new IllegalArgumentException("I need a MainPresenter");
else mainPresenter = (MainPresenter) context;
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
init();
mainPresenter.bind(this);
mainPresenter.setup();
}
public MainViewImpl(Context context, AttributeSet attrs) {
super(context, attrs);
if(!(context instanceof MainPresenter))
throw new IllegalArgumentException("I need a MainPresenter");
else mainPresenter = (MainPresenter) context;
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
init();
mainPresenter.bind(this);
mainPresenter.setup();
}
public MainViewImpl(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if(!(context instanceof MainPresenter))
throw new IllegalArgumentException("I need a MainPresenter");
else mainPresenter = (MainPresenter) context;
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
init();
mainPresenter.bind(this);
mainPresenter.setup();
}
@Override
public void setTitle(String title) {
tvTitle.setText(title);
}
@Override
public void setBackgroundColor(String backgroundColor) {
super.setBackgroundColor(Color.parseColor(backgroundColor));
}
private void init(){
inflater.inflate(R.layout.main_view_layout, this, true);
tvTitle = (TextView) findViewById(R.id.tvTitle);
btnAwkward = (Button) findViewById(R.id.btnAwkward);
btnAwkward.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mainPresenter.awkwardButtonPressed();
}
});
}
}
MainActivity
public class MainActivity extends AppCompatActivity implements MainPresenter {
private MainView mainView;
private final Map<String, String> status = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
if (savedInstanceState.containsKey("title")) {
status.put("title", savedInstanceState.getString("title"));
}
if (savedInstanceState.containsKey("backgroundColor")) {
status.put("backgroundColor", savedInstanceState.getString("backgroundColor"));
}
} else {
status.put("title", "My Cool MVP");
status.put("backgroundColor", "#00AAFF");
}
setContentView(R.layout.activity_main);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("title", status.get("title"));
outState.putString("backgroundColor", status.get("backgroundColor"));
super.onSaveInstanceState(outState);
}
@Override
public void bind(MainView mainView) {
this.mainView = mainView;
}
@Override
public void setup() {
this.mainView.setTitle(status.get("title"));
this.mainView.setBackgroundColor(status.get("backgroundColor"));
}
@Override
public void awkwardButtonPressed() {
status.put("backgroundColor", "#00FFAA");
status.put("title", "That Was Awkward...");
mainView.setBackgroundColor(status.get("backgroundColor"));
mainView.setTitle(status.get("title"));
}
}
Infine le risorse xml per i layout qui potrebbero essere:
main_activity_layout.xml
<com.sample.application.MainViewImpl
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cvMainView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
main_view_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btnAwkward"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/awkward_button"/>
</LinearLayout>
In questo modo View
è la vista e MainViewImpl
è il presentatore. Nota in entrambe le implementazioni la controparte è sempre accessibile attraverso la sua interfaccia, il Activity
mantiene lo stato in una mappa che inserisce Activity
quando necessario e recupera nuovamente quando viene ricreato.
Domande
Ha senso? Questo è davvero MVP? In caso contrario, qual è il ruolo effettivo della classe SavedInstanceState
in MVP Android? Quali potrebbero essere i difetti dell'implementazione presentata?
Nel caso qualcuno stia vagando su come testare la logica del presentatore (che ora è un Activity
), ecco come può essere fatto:
MainActivityTest
public class MainActivityTest {
MainPresenter mainPresenter;
MainView mainView;
@Before
public void setUp() {
mainPresenter = new MainActivity();
mainView = Mockito.mock(MainView.class);
mainPresenter.bind(mainView);
}
@Test
public void testSetup() throws Exception {
mainPresenter.setup();
Mockito.verify(mainView).setTitle(Mockito.anyString());
Mockito.verify(mainView).setBackgroundColor(Mockito.anyString());
}
@Test
public void testAwkwardPressing() throws Exception {
mainPresenter.awkwardButtonPressed();
Mockito.verify(mainView).setTitle("That Was Awkward...");
Mockito.verify(mainView).setBackgroundColor("#00FFAA");
}
}