Saltar al contenido

MVP con pruebas – Parte 1 (Arquitectura MVP)

El proyecto demuestra la labor de una actividad que recibe y muestra una lista de películas de una solicitud de red (que se aplica localmente para su demostración).

Esta es la representación visual de la estructura del proyecto:

MVP con pruebas – Parte 1 (Arquitectura MVP)
MVP con pruebas – Parte 1 (Arquitectura MVP)
123456789package com.example.pavneet_singh.mvptestingdemo.mvp.movieslist;------------------------------------------------------------------------------------------------.model : POJO y Repository pattern.mvp.movielist : Implementación MVP para Movie List Screen.util : Para imitar una respuesta de la red y proveer clases útiles
  1. Implementación de la vista pasiva: Una vista debe implementarse como una vista pasiva. MoviesListActivity implementa la interfaz MoviesListContract.View para exponer las funcionalidades a otros componentes como el presentador.
12/// passive viewpublicclassMoviesListActivityextendsAppCompatActivityimplementsMoviesListContract.View{

java

El presentador mantendrá una referencia para ver e invocar los métodos expuestos por la interfaz View. Este enfoque de la vista pasiva simplifica la fase de prueba con la ayuda de un objeto burlón.

  1. Presentador gratuito de la API de Android : El presentador maneja los eventos desde la interfaz de usuario y utiliza el mecanismo de devolución de llamada de la interfaz para comunicarse con largos hilos y procesos de fondo. El presentador implementa el MoviesListContract.Presenter
1clase final públicaPelículaPresentación de películasPresentación de películasListaContrato.Presentación{

java

El punto clave para hacer un presentador comprobable es evitar el uso de la API de androides porque a menudo la API común de androides como Sharedpreference, base de datos requiere un contexto que no puede ser adquirido sin la participación del ciclo de vida de la actividad por lo que para llevar a cabo las pruebas de junit local a través de JVM en IDE el marco de mockito se utiliza para crear objeto ficticio.

  1. Un contrato completo para la Vista y el Presentador: Las interfaces de la Vista y el Presentador se combinan en una sola interfaz, conocida como Contrato, que se utilizará para establecer una relación entre las implementaciones concretas de la Vista y el Presentador.

MoviesListContract.java

1234567891011121314151617181920212223242526interfaz públicaMoviesListContract{// implementada por MoviesListActivity para proporcionar una implementación concretainterfazView{evita el showProgress(); // mostrar la barra de progresovoidhideProgress();// ocultar la barra de progreso evita mostrarMovieList(List<Movie;Movie;// recibir respuesta para mostrar evita mostrarLoadingError(String errMsg); // mostrar error}// implementado por MoviesPresenter para manejar el evento de usuariointerfazPresenter{voidloadMoviewList();voiddropView();}// implementado por MoviePresenter para recibir respuesta de procesos asincrónicosinterfazOnResponseCallback{voidonResponse(List<Movie> movies);voidonError(String errMsg);}}

java

La interfaz View simplemente indica el comportamiento de la UI y una implementación concreta de Presenter gestionará el estado de la UI invocando los métodos de View.

Un presentador tiene que realizar más métodos específicos de acción de usuario que la operación orientada a resultados en los datos. El presentador también utiliza las interfaces de OnResponseCallback para establecer un vínculo de comunicación con las tareas y procesos de fondo.

La razón para mantener Presentador y Vista en una sola interfaz es

  • View también es una clase en Android. Así que, con esto podemos nombrar nuestra interfaz como View sin crear ninguna confusión.
  • Tanto la función de vista como la de presentador están disponibles en un solo lugar, lo que mejora la claridad.
  • Presentador y Vista, ambos tienen un comportamiento específico para servir a una sola entidad (actividad o fragmento) por lo que tiene más sentido mantener las mismas cosas juntas.

Punto importante al hacer un presentador

  • No cree un ciclo de vida en el presentador, de lo contrario su presentador estará estrechamente conectado con el componente Androide donde la mayoría tiene sus propias diferencias. Esto hace que el presentador sea más dependiente y altamente rígido a las modificaciones.
  • Introducir un método en el presentador que acepte la referencia de la vista requiere comprobaciones nulas en muchos lugares, así que evítelo si puede y en su lugar utilice el constructor.
  • Usar un mecanismo como el LRUCache para retener los datos, mantener al presentador apátrida y dejarlo recrear con la creación de una actividad o fragmento.
  1. Ver y Presentar en Conectividad : MoviePresenter recibirá la referencia de View, es decir MoviesListActivity, a través del constructor junto con la implementación concreta de la interfaz MovieRepo que contendrá la lógica para recuperar datos de una fuente de datos (red, base de datos local, archivos, etc.).

A continuación se muestra la implementación concreta del presentador, es decir, MoviePresenter.java:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758clase final públicaMoviePresenterimplementsMoviesListContract.Presenter{// para mantener la referencia para ver el privadoMoviesListContract. Ver vista;// Patrón de repositorio, mcliente mantiene referencia a la implementación de recuperación de datos concretosprivadoMovieRepo mcliente;públicoMoviePresenter(MoviesListContract.View view,MovieRepo client){esto. view = view; mclient = client;}@OverridepublicvoiddropView(){ view =null;}// sería activado por MovieListActivity@OverridepublicvoidloadMoviewList(){ view.showProgress(); mclient. getMovieList(callback);// requerido para la prueba de la IU del expreso// esperar hasta que la respuesta ocurraEspressoTestingIdlingResource. increment();}// mecanismo de devolución de llamada , onResponse será activado con response// por simulatemovieclient(o su red o proceso de base de datos) y pasar la respuesta a viewprivatefinalOnResponseCallback callback =newOnResponseCallback(){@OverridepublicvoidonResponse(List<Movie> movies){ view. showMovieList(movies); view.hideProgress();EspressoTestingIdlingResource.decrement();}@OverridepublicvoidonError(String errMsg){ view.hideProgress(); view.showLoadingError(errMsg);EspressoTestingIdlingResource.decrement();}};}

java

El método loadMoviewList sería activado por MoviesListActivity cuando el usuario realiza la acción (deslizar hacia abajo en la pantalla) entonces se mostrará una barra de progreso en la pantalla hasta que se reciba una respuesta y se entregue a la actividad por view.showMovieList(movies); método para mostrar la lista.

Ver{privadoRecicladorVer recicladorVer; privadoPelículasAdaptador de películasAdaptador; privadoPeladoRefrescoPeladoPelado; privadoPelículasListaContrato. Presentador presentador;privadoVista de texto tv_empty_msg;@OverrideprotegidoevitarCrear(Paquete guardadoInstanceState){super.onCrear(guardadoInstanceState);setVista de contenido(R.layout. activity_movie_list);initViews();}privatevoidinitViews(){ presenter =newMoviePresenter(this,newSimulateMovieClient()); recyclerView =(RecyclerView)findViewById(R.id. movies_recycler_list);// list tv_empty_msg =(TextView)findViewById(R.id.swipe_msg_tv);// empty message recyclerView.setLayoutManager(newLinearLayoutManager(this));// for vertical liner list moviesAdapter =newMoviesAdapter(this); recyclerView. setAdapter(moviesAdapter); swipeLayout =(SwipeRefreshLayout)findViewById(R.id.swipe_container); swipeLayout.setOnRefreshListener(listenener); swipeLayout. setColorSchemeColors(// colores para el diálogo de progresoContextCompat.getColor(MoviesListActivity.this,R.color.colorPrimary),ContextCompat.getColor(MoviesListActivity.this,R.color.colorAccent),ContextCompat. getColor(MoviesListActivity.this, android.R.color.holo_green_light));}privatefinalOnRefreshListener listener =newOnRefreshListener(){@OverridepublicvoidonRefresh(){ presenter. loadMoviewList();}};// para el futuro, para mostrar el progreso@Overridepublic evitahowProgress(){ swipeLayout.setRefreshing(true);}@OverridepublicvoidhideProgress(){ swipeLayout. setRefreshing(false);}// conmutar la visibilidad de la vista de texto vacío o de la lista// mostrar la lista sólo cuando la respuesta no está vacíaprivate evita mostrarORHideListView(bandera booleana){if(flag){ tv_empty_msg. setVisibility(View.GONE); recyclerView.setVisibility(View.VISIBLE);}else{ tv_empty_msg.setVisibility(View.VISIBLE); recyclerView.setVisibility(View.GONE);}}@Overridepublic evita mostrarMovieList(List<Movie> movies){if(!movies.isEmpty()){ moviesAdapter. setList(movies);showORHideListView(true);}}@Overridepublic evita mostrarLoadingError(String errMsg){hideProgressAndShowErr(errMsg);showORHideListView(false);}privatevoidhideProgressAndShowErr(String msg){ tv_empty_msg. setVisibility(View.VISIBLE);Toast.makeText(this, msg,Toast.LENGTH_SHORT).show();showORHideListView(false);}@OverrideprotectedonDestroy(){super.onDestroy(); presenter.dropView();}}
java

La implementación anterior de View crea una referencia al presentador y pasa su referencia usando esto junto con la instancia SimulateMovieClient, que contiene la lógica de recuperación de datos.

lista_de_actividades.xml

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455<?xml version="1.0" encoding="utf-8"? RelativeLayoutxmlns:androandroid:android:layout_width="match_parent "android:layout_height="match_parent "android:scrollbars="vertical"<TextViewandroid: text="@string/No_Data_MSG "android:gravity="center|center_horizontal "android:android:layout_width="match_parent "android:layout_height="match_parent"/;<android. support.v4.widget.SwipeRefreshLayoutandroid:android:layout_width="match_parent "android:layout_height="match_parent"<android.support.v7.widget. RecicladorViewandroid:android:visibility="gone "android:layout_width="match_parent "android:layout_height="wrap_content"/;</android.support.v4.widget.SwipeRefreshLayout
xml

Este es el código del adaptador para crear elementos de la lista

MoviesAdapter.java:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117publicclassMoviesAdapterextendsRecyclerView. Adaptador<MoviesAdapter. MovieHolder&;{privatefinalContexto contextual;privatefinalList<Movie&; movStrings =newArrayList<;();privatestaticfinalString TAG ="MoviesAdapter";publicMoviesAdapter(Contexto contextual){esto. context = context;}@OverridepublicMovieHolderonCreateViewHolder(ViewGroup parent,int viewType){View view =LayoutInflater.from(context).infllate(R.layout. item_movie_model,parent,false);returnnnewMovieHolder(view);}@OverridepublicvoidonBindViewHolder(MovieHolder,finalint position){Log.e(TAG, "onBindViewHolder: "+position); holder. movieTitle.setText(movStrings.get(position).getTitle()); holder.date.setText(Utility.convertMinutesToDuration(movStrings.get(position).getDurationinMinutes())); holder.rating.setText(Double. toString(movStrings.get(posición).getRating())); holder.moviesLayout.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){Toast.makeText(context, movStrings. get(position).toString(),Toast.LENGTH_SHORT).show();}});}publicvoidsetList(List<Movie> list){ movStrings.clear(); movStrings.addAll(list);notifyDataSetChanged();Log.e(TAG, "onNext: "+movStrings.size());}@OverridepublicintgetItemCount(){regresar movStrings.size();}clase estática públicaMovieHolderextendsRecyclerView.ViewHolder{TextView movieTitle;TextView date;TextView view;TextView rating;LinearLayout moviesLayout;publicMovieHolder(View v){super(v); moviesLayout =(LinearLayout) v. findViewById(R.id.movies_layout); movieTitle =(TextView) v.findViewById(R.id.title); date =(TextView) v.findViewById(R.id.date); rating =(TextView) v.findViewById(R.id.rating);}}}

java

El archivo de diseño de artículos item_movie_model.xml:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129< ? xml version="1.0" encoding="utf-8"?<LinearLayoutxmlns:androxmlns:tools="http://schemas.android.com/tools "android:android:layout_width="match_parent "android:layout_height="wrap_content "android:background="? androide:attr/seleccionableItemBackground "androide:gravity="center_vertical "android:minHeight="72dp "androide:orientation="horizontal "androide:padding="16dp"<LinearLayoutandroid:layout_width="0dp "android:layout_height="wrap_content "android:layout_weight="1 "androide:orientation="vertical"<TextViewandroid: tools_text="Avengers "android:layout_width="wrap_content "android:layout_height="wrap_content "android:layout_gravity="top "android:paddingRight="16dp "android:textStyle="bold "android:textColor="@android:color/black "android:textSize="16sp"/><TextViewandroid:android: layout_width="wrap_content "android:layout_height="wrap_content "android:maxLines="1 "android:paddingRight="16dp "android: textColor="@color/greyLight"/;</LinearLayout=;<LinearLayoutandroid:layout_width="wrap_content "android:layout_height="35dp "android:orientation="horizontal"<ImageViewandroid: android_layout_width="15dp "android:layout_height="15dp "android:scaleType="centerCrop "android:src="@drawable/star "android: tint="@color/colorAccent"/;<TextViewandroid:android:layout_width="wrap_content "android:layout_height="wrap_content "android:layout_marginLeft="8dp "tools:text="5. 0 "android:layout_marginStart="8dp"/;/LinearLayout=;

xml

MovieRepo es una interfaz implementada por SimulateMovieClient para imitar el comportamiento del hilo de fondo para recuperar datos como se muestra a continuación.

MovieRepo.java

12345publicinterfaceMovieRepo{voidgetMovieList(MoviesListContract.OnResponseCallback callback);}

java

SimulateMovieClient contiene una implementación de hilo que dará la respuesta con un retraso de 1500 milisegundos.

123456789101112131415161718192021222324252627282930313233343536373839404142434445publicfinalclassSimulateMovieClientimplementsMovieRepo{staticfinalRandom RANDOM =newRandom();publicvoidgetMovieList(finalMoviesListContract. OnResponseCallback callback){// Para imitar la solicitud de la red delaynewHandler(). postDelayed(newRunnable(){@Overridepublicvoidrun(){ArrayList<Movie > movies =newArrayList<&gt);try{ movies.add(newMovie(RANDOM.nextInt(Integer.MAX_VALUE), "IT",Utility. convertStringToDate("2017-10-8"),7.6,127,Type.HORROR));// add more Movie object in list callback.onResponse(movies);}catch(ParseException e){ callback.onError("Error from network");}}},1500);}}

java

Salida visual final :

Esto concluye la implementación introductoria del MVP y la estructura completa del código está disponible en el repositorio github para jugar.