Slik adopterer du Model View Presenter på Android

I den forrige veiledningen snakket vi om Model View Presenter-mønsteret, hvordan det brukes på Android, og hva de viktigste fordelene er. I denne opplæringen undersøker vi Model View Presenter-mønsteret mer detaljert ved å implementere det i en Android-applikasjon.

I denne opplæringen:

  • Vi bygger en enkel applikasjon ved hjelp av MVP-mønsteret
  • Vi undersøker hvordan du implementerer MVP-mønsteret på Android
  • og vi diskuterer hvordan du skal overvinne noen vanskeligheter forårsaket av Android-arkitekturen

1. Modellvisning Presentator

Model View Presenter-mønsteret er et arkitektonisk mønster basert på modellvisningskontrollmønsteret (MVC) som øker adskillelsen av bekymringer og letter enhetstesting. Det skaper tre lag, Modell, Utsikt, og Presenter, hver med et veldefinert ansvar.

Modellen holder forretningslogikken til søknaden. Den styrer hvordan data opprettes, lagres og endres. Visningen er et passivt grensesnitt som viser data og ruter brukerhandlinger til presentatøren. Presentatøren fungerer som mellommann. Den henter data fra modellen og viser den i visningen. Den behandler også brukerhandlinger videresendt av visningen.

2. Prosjektplanlegging og oppsett

Vi skal bygge en enkel notater søknad for å illustrere MVP. Appen lar brukeren ta notater, lagre dem i en lokal database og slette notater. For å gjøre det enkelt, har appen bare én aktivitet.

I denne opplæringen konsentrerer vi seg primært om implementeringen av MVP-mønsteret. Andre funksjoner, for eksempel å sette opp en SQLite-database, bygge en DAO, eller håndtere brukerinteraksjon, hoppes over. Hvis du trenger hjelp med noen av disse emnene, har Envato Tuts + noen gode opplæringsprogrammer om disse emnene.

Handlingsdiagram og MVP-lag

La oss begynne med opprettelsen av et nytt notat. Hvis vi bryter denne handlingen inn i mindre operasjoner, så er dette hva strømmen ser ut som å bruke MVP arkitektonisk mønster:

  • Brukeren skriver et notat og klikker på legg til notatknappen.
  • Presentatøren skaper en Merk objekt med teksten som er oppgitt av brukeren og ber Modellen å sette den inn i databasen.
  • Modellen legger inn notatet i databasen og informerer presentatøren om at listen over notater har endret seg.
  • Presentatøren sletter tekstfeltet og ber deg om å oppdatere listen for å vise det nyopprettede notatet.

MVP-grensesnitt

La oss nå vurdere operasjonene som trengs for å oppnå denne handlingen, og skille dem fra MVP. For å holde de forskjellige objektene løst koblet, tar kommunikasjonen mellom lagene sted ved å bruke grensesnitt. Vi trenger fire grensesnitt:

  • RequiredViewOps: obligatorisk Se operasjoner tilgjengelig for Presenter
  • ProvidedPresenterOps: operasjoner tilbudt til visning for kommunikasjon med presentatør
  • RequiredPresenterOps: Påkrevd Presentasjonsoperasjoner tilgjengelig for Modell
  • ProvidedModelOps: Operasjoner som tilbys til Model for å kommunisere med Presenter

3. Implementere MVP på Android

Nå som vi har en ide om hvordan de ulike metodene skal organiseres, kan vi begynne å lage vår app. Vi forenkler implementeringen ved bare å fokusere på handlingen for å legge til et nytt notat. Kildefilene til denne opplæringen er tilgjengelige på GitHub.

Vi bruker bare en Aktivitet med et oppsett som inkluderer:

  • EditText for nye notater
  • Knapp å legge til et notat
  • RecyclerView å liste alle notater
  • to TextView elementer og a Knapp inne i a RecyclerView holder

grensesnitt

La oss begynne med å skape grensesnittene. For å holde alt organisert, plasserer vi grensesnittene i en holder. Igjen, i dette eksempelet fokuserer vi på handlingen for å legge til et nytt notat.

offentlig grensesnitt MVP_Main / ** * Obligatorisk visningsmetoder tilgjengelig for Presenter. * Et passivt lag, ansvarlig for å vise data * og motta brukerinteraksjoner * / grensesnitt RequiredViewOps // Se operasjoner tillatt til Presenter Context getAppContext (); Kontekst getActivityContext (); void notifyItemInserted (int layoutPosition); void notifyItemRangeChanged (int posisjonStart, int itemCount);  / ** * Operasjoner som tilbys til Vis for å kommunisere med Presenter. * Behandler brukerinteraksjoner, sender dataforespørsler til modell, etc. * / grensesnitt ProvidedPresenterOps // Presenter operasjoner tillatt å vise ugyldig clickNewNote (EditText editText); // sette opp resirkuleringsadapter int getNotesCount (); NotesViewHolder createViewHolder (ViewGroup foreldre, int viewType); void bindViewHolder (NotesViewHolder holder, int posisjon);  / ** * Påkrevd Presentasjonsmetoder tilgjengelig for Model. * / grensesnitt RequiredPresenterOps // Presenter operasjoner tillatt til Model Context getAppContext (); Kontekst getActivityContext ();  / ** * Operasjoner som tilbys til Model for å kommunisere med Presenter * Håndterer all data business logic. * / grensesnitt ProvidedModelOps // Modelloperasjoner tillatt til Presenter int getNotesCount (); Merk getNote (int posisjon); int insertNote (Notat notat); boolsk loadData (); 

Se lag

Det er nå på tide å lage modell, visning og presentasjonslag. Siden Hoved aktivitet vil fungere som Vis, den bør implementere RequiredViewOps grensesnitt.

offentlig klasse MainActivity utvider AppCompatActivity implementerer View.OnClickListener, MVP_Main.RequiredViewOps private MVP_Main.ProvidedPresenterOps mPresenter; privat EditText mTextNewNote; privat ListNotes mListAdapter; @Override public void onClick (Vis v) switch (v.getId ()) tilfelle R.id.fab: // legger til et nytt notat mPresenter.clickNewNote (mTextNewNote);  @ Overstyr offentlig kontekst getActivityContext () returner dette;  @ Overstyr offentlig kontekst getAppContext () return getApplicationContext ();  // Gi beskjed om RecyclerAdapter som et nytt element ble satt inn @ Overstyr offentleg tomt informerItemInserted (int adapterPos) mListAdapter.notifyItemInserted (adapterPos);  // varsle RecyclerAdapter at elementene har endret @Override public void notifyItemRangeChanged (int posisjonStart, int itemCount) mListAdapter.notifyItemRangeChanged (posisjonStart, itemCount);  // varsle RecyclerAdapter at datasettet er endret @Override public void notifyDataSetChanged () mListAdapter.notifyDataSetChanged ();  // Recycler adapter // Denne klassen kan ha sin egen Presenter, men for enkelhets skyld skyldes det bare en Presenter. // Adapteren er passiv og all behandling skjer // i Presenter-laget. Private klasse ListNotes utvider RecyclerView.Adapter @Override public int getItemCount () return mPresenter.getNotesCount ();  @ Overstyr offentlige notaterViewHolder onCreateViewHolder (ViewGroup foreldre, int viewType) return mPresenter.createViewHolder (foreldre, viewType);  @Override public void onBindViewHolder (NotesViewHolder holder, int posisjon) mPresenter.bindViewHolder (holder, posisjon); 

Presenter Layer

Presentatøren er mellommann og trenger å implementere to grensesnitt:

  • ProvidedPresenterOps for å tillate anrop fra visningen
  • RequiredPresenterOps å motta resultater fra modellen

Vær spesielt oppmerksom på View-lagreferansen. Vi må bruke en WeakReference siden Hoved aktivitet kan bli ødelagt når som helst, og vi vil unngå minnelekkasje. Modelllagret har heller ikke blitt satt opp ennå. Vi gjør det senere når vi kobler sammen MVP-lagene sammen.

offentlig klasse MainPresenter implementerer MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps // Se referanse. Vi bruker som WeakReference // fordi aktiviteten kan ødelegges når som helst // og vi ønsker ikke å lage en hukommelseskap privat WeakReference mVIS; // Modell referanse privat MVP_Main.ProvidedModelOps mMmodel; / ** * Presenter Constructor * @param view MainActivity * / public MainPresenter (MVP_Main.RequiredViewOps visning) mView = new WeakReference <> (visning);  / ** * Returner visningsreferansen. * Kast et unntak hvis visningen ikke er tilgjengelig. * / privat MVP_Main.RequiredViewOps getView () kaster NullPointerException hvis (mView! = null) returner mView.get (); ellers kaster du ny NullPointerException ("Vis er utilgjengelig");  / ** * Henter totalt Notes teller fra Model * @return Notater listestørrelse * / @Override public int getNotesCount () return mModel.getNotesCount ();  / ** * Oppretter RecyclerView-holderen og konfigurerer sin visning * @param foreldre Recycler viewGroup * @param viewType Holder type * @return Recycler ViewHolder * / @Override public NotesViewHolder createViewHolder (ViewGroup foreldre, int viewType) NotesViewHolder viewHolder; LayoutInflater inflater = LayoutInflater.from (parent.getContext ()); View viewTaskRow = inflater.inflate (R.layout.holder_notes, foreldre, false); viewHolder = nye NotesViewHolder (viewTaskRow); returnere viewHolder;  / ** * Binder ViewHolder med RecyclerView * @param holder Holder for å binde * @param posisjon Plassering på Recycler adapter * / @Override public void bindViewHolder (endelig NotesViewHolder holder, int posisjon) endelig Note note = mModel.getNote (posisjon) ; holder.text.setText (note.getText ()); holder.date.setText (note.getDate ()); holder.btnDelete.setOnClickListener (new View.OnClickListener () @Override public void onClick (Vis v) clickDeleteNote (notat, holder.getAdapterPosition (), holder.getLayoutPosition ()););  / ** * @return Applikasjonskontekst * / @Override public Context getAppContext () prøv return getView (). getAppContext ();  fangst (NullPointerException e) return null;  / ** * @return Aktivitetskontekst * / @Override public Context getActivityContext () prøv return getView (). getActivityContext ();  fangst (NullPointerException e) return null;  / ** * Kalt av Vis når bruker klikker på ny Notat-knapp. * Oppretter en notat med tekst skrevet av brukeren og spør * Modell for å sette den inn i DB. * @param editText EditText med tekst skrevet av brukeren * / @Override public void clickNewNote (endelig EditText editText) getView (). showProgress (); siste String noteText = editText.getText (). toString (); hvis (! noteText.isEmpty ()) ny AsyncTask() @Overtrid beskyttet Integer doInBackground (Void ... params) // Innsetter notat i Modell, returadapterposisjon returnerer mModel.insertNote (makeNote (noteText));  @Override protected void onPostExecute (Integer adapterPosition) prøv hvis (adapterPosition> -1) // Merk innført getView (). ClearEditText (); getView (). notifyItemInserted (adapterposisjon + 1); getView (). notifyItemRangeChanged (adapterposisjon, mModel.getNotesCount ());  else // Informerer om feil getView (). hideProgress (); getView (). showToast (makeToast ("Feil opprette notat [" + noteText + "]"));  fangst (NullPointerException e) e.printStackTrace ();   .henrette();  ellers prøv getView (). showToast (makeToast ("Kan ikke legge til et tomt notat!"));  fangst (NullPointerException e) e.printStackTrace ();  / ** * Oppretter et Objektobjekt med gitt tekst * @param noteText String med Notattekst * @return En Objektobjekt * / offentlig Notatnotat (Stringnotatekst) Notat notat = Ny Notat (); note.setText (noteText); note.setDate (getDate ()); retur notat; 

Modelllag

Modelllaget er ansvarlig for håndtering av forretningslogikken. Den har en Arraylist med notatene lagt til i databasen, en DAO-referanse for å utføre databaseoperasjoner, og en referanse til presentatøren.

offentlig klasse MainModel implementerer MVP_Main.ProvidedModelOps // Presenter referanse privat MVP_Main.RequiredPresenterOps mPresenter; privat DAO mDAO; // Recycler data offentlig ArrayList mNotes; / ** * Hovedkonstruent, kalt Aktivitet under MVP-oppsett * @param presentator Presenter instance * / public MainModel (MVP_Main.RequiredPresenterOps presentator) this.mPresenter = presenterer; mDAO = ny DAO (mPresenter.getAppContext ());  / ** * Setter inn et notat på DB * @param notat Merk for å sette inn * @return Obs posisjon på ArrayList * / @Override public int insertNote (Notat) Note insertedNote = mDAO.insertNote (note); hvis (insertedNote! = null) loadData (); returnere getNotePosition (insertedNote);  return -1;  / ** * Laster alle data, får notater fra DB * @return sant med suksess * / @Override offentlig boolean loadData () mNotes = mDAO.getAllNotes (); returner mNotes! = null;  / ** * Får et bestemt notat fra notatlisten ved hjelp av sin array posisjon * @param posisjon Array posisjon * @return Notat fra listen * / @Override offentlig Note getNote (int posisjon) return mNotes.get (posisjon);  / ** * Få ArrayList størrelse * @return ArrayList størrelse * / @Override public int getNotesCount () hvis (mNotes! = Null) returner mNotes.size (); returner 0; 

4. Tynger alt sammen

Med MVP-lagene på plass, må vi instantiere dem og sette inn de nødvendige referansene. Før vi gjør det, må vi ta opp noen problemer som er direkte relatert til Android.

Instantiating lagene

Fordi Android ikke tillater instantiering av en Aktivitet, Visningslaget vil bli instantiated for oss. Vi er ansvarlige for å instantiere presentasjons- og modelllagene. Dessverre instantiating disse lagene utenfor Aktivitet kan være problematisk.

Det anbefales å bruke en form for beredskapsinjeksjon for å oppnå dette. Siden vårt mål er å konsentrere seg om MVP-implementeringen, vil vi ta en enklere tilnærming. Dette er ikke den beste tilnærmingen som er tilgjengelig, men det er lettest å forstå. Vi diskuterer MVP og avhengighetsinjeksjon senere i denne serien.

  • Installer presentatoren og modellen i aktiviteten ved hjelp av lokale variabler
  • sette opp RequiredViewOps og ProvidedModelOps i presentatøren
  • sette opp RequiredPresenterOps i modellen
  • lagre ProvidedPresenterOps som referanse til bruk i visningen
/ ** * Setup Model View Presenter mønster * / private void setupMVP () // Opprett Presenter MainPresenter presenterer = ny MainPresenter (dette); // Opprett Model MainModel-modellen = ny MainModel (presentator); // Sett Presenter modell presenterer. SetModel (modell); // Sett presentatøren som et grensesnitt mPresenter = presentator; 

Håndteringskonfigurasjonsendringer

En annen ting vi bør vurdere er aktivitetens livssyklus. Android Aktivitet kan ødelegges når som helst, og presentasjons- og modelllagene kan også bli ødelagt med det. Vi må fikse dette ved å bruke en slags statlig maskin for å lagre tilstand under konfigurasjonsendringer. Vi bør også informere de andre lagene om aktivitetens tilstand.

For å oppnå dette, bruker vi en egen klasse, StateMaintainer, som inneholder et fragment som opprettholder sin tilstand og bruker dette fragmentet til å lagre og hente gjenstandene våre. Du kan se på implementeringen av denne klassen i kildefilene i denne opplæringen.

Vi må legge til en onDestroy metode til presentatøren og modellen for å informere dem om aktivitetens nåværende tilstand. Vi må også legge til en setview metode til presentatøren, som vil være ansvarlig for å motta en ny visning av den gjenopprettede aktiviteten.

offentlig klasse MainActivity utvider AppCompatActivity implementerer View.OnClickListener, MVP_Main.RequiredViewOps // ... private void setupMVP () // Sjekk om StateMaintainer er opprettet hvis (mStateMaintainer.firstTimeIn ()) // Opprett presentatøren MainPresenter presenterer = ny MainPresenter (dette); // Opprett Model MainModel-modellen = ny MainModel (presentator); // Sett Presenter modell presenterer. SetModel (modell); // Legg til presentator og modell til StateMaintainer mStateMaintainer.put (presentator); mStateMaintainer.put (modell); // Sett presentatøren som et grensesnitt // For å begrense kommunikasjonen med den mPresenter = presentatør;  // få presentatøren fra StateMaintainer annet // Få presentatøren mPresenter = mStateMaintainer.get (MainPresenter.class.getName ()); // Oppdatert visning i Presenter mPresenter.setView (dette);  // ...

Konklusjon

MVP-mønsteret er i stand til å løse noen problemer forårsaket av Android standardarkitektur. Det gjør at koden din er lett å vedlikeholde og teste. Å vedta MVP kan kanskje se vanskelig ut først, men når du forstår logikken bak den, er hele prosessen enkel.

Du kan nå lage ditt eget MVP-bibliotek eller bruke en løsning som allerede er tilgjengelig, for eksempel Mosby eller simple-mvp. Du bør nå bedre forstå hva disse bibliotekene gjør bak kulissene.

Vi er nesten på slutten av vår MVP-reise. I den tredje og siste delen av denne serien legger vi til enhetstesting i blandingen og tilpasser koden vår for å bruke avhengighetsinjeksjon med Dagger. Jeg håper å se deg der.