vista presentador patron mvc modelo ejemplo arquitectura android model-view-controller architecture mvvm

presentador - Arquitectura de aplicaciones de Android-MVVM o MVC?



mvvm android ejemplo (2)

Tengo un proyecto de Android en el que estoy empezando a trabajar, y quiero que su estructura sea lo más robusta posible.

Vengo de un fondo WPF MVVM y he estado leyendo un poco sobre la arquitectura de aplicaciones de Android, pero no pude encontrar una respuesta clara y directa sobre qué arquitectura debería usar.

Algunas personas sugirieron usar MVVM - http://vladnevzorov.com/2011/04/30/android-application-architecture-part-ii-architectural-styles-and-patterns/

y otros sugirieron usar MVC, pero no especificaron cómo exactamente debería implementarse.

Como dije, procedo de un entorno WPF-MVVM y, por lo tanto, sé que depende en gran medida de enlaces que, por lo que yo sé, no son compatibles por defecto en Android.

Parece que hay una solución de terceros: http://code.google.com/p/android-binding/ Pero no sé si me gustaría confiar en eso. ¿Qué sucede si su desarrollo se detiene y no será compatible con futuras API y etc.

Básicamente lo que estoy buscando es un tutorial completo que me enseñará las mejores prácticas para construir la estructura de la aplicación. Estructura de carpetas y clases, etc. Simplemente no pude encontrar ningún tutorial exhaustivo, y esperaba que Google proporcionara tal tutorial para sus desarrolladores. Simplemente no creo que este tipo de documentación maneje el aspecto técnico lo suficientemente bueno - http://developer.android.com/guide/topics/fundamentals.html

Espero haber sido lo suficientemente claro y no estoy pidiendo demasiado, solo quiero estar seguro de la estructura de mi aplicación, antes de que mi código se convierta en un monstruo de espagueti.

¡Gracias!


Creo que sería más útil explicar MVVM en Android a través de un ejemplo. El artículo completo junto con la información de repo de GitHub está here para más información.

Supongamos el mismo ejemplo de aplicación de película de referencia introducido en la primera parte de esta serie. El usuario ingresa un término de búsqueda para una película y presiona el botón ''ENCONTRAR'', en función del cual la aplicación busca la lista de películas que incluye ese término de búsqueda y las muestra. Al hacer clic en cada película en la lista, se muestran sus detalles.

Ahora explicaré cómo se implementa esta aplicación en MVVM seguido de la aplicación completa de Android, que está disponible en mi página de GitHub .

Cuando el usuario hace clic en el botón ''ENCONTRAR'' en la Vista, se llama a un método desde ViewModel con el término de búsqueda como su argumento:

main_activity_button.setOnClickListener({ showProgressBar() mMainViewModel.findAddress(main_activity_editText.text.toString()) })

El ViewModel luego llama al método findAddress del Modelo para buscar el nombre de la película:

fun findAddress(address: String) { val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() { override fun onSuccess(t: List<MainModel.ResultEntity>) { entityList = t resultListObservable.onNext(fetchItemTextFrom(t)) } override fun onError(e: Throwable) { resultListErrorObservable.onNext(e as HttpException) } }) compositeDisposable.add(disposable) }

Cuando la respuesta proviene del Modelo, el método onSuccess del observador RxJava lleva el resultado exitoso, pero como ViewModel es View agnostic, no tiene ni usa ninguna instancia de View para pasar el resultado que se muestra. En su lugar desencadena un evento en el resultListObservable llamando a resultListObservable.onNext (fetchItemTextFrom (t)), que se observa por la vista:

mMainViewModel.resultListObservable.subscribe({ hideProgressBar() updateMovieList(it) })

Entonces, lo observable juega un rol de mediador entre View y ViewModel:

  • ViewModel desencadena un evento en su observable
  • Ver actualizaciones de la interfaz de usuario suscribiéndose al observable de ViewModel

Aquí está el código completo de la Vista. En este ejemplo, View es una clase Activity, pero Fragment también se puede usar por igual:

class MainActivity : AppCompatActivity() { private lateinit var mMainViewModel: MainViewModel private lateinit var addressAdapter: AddressAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mMainViewModel = MainViewModel(MainModel()) loadView() respondToClicks() listenToObservables() } private fun listenToObservables() { mMainViewModel.itemObservable.subscribe(Consumer { goToDetailActivity(it) }) mMainViewModel.resultListObservable.subscribe(Consumer { hideProgressBar() updateMovieList(it) }) mMainViewModel.resultListErrorObservable.subscribe(Consumer { hideProgressBar() showErrorMessage(it.message()) }) } private fun loadView() { setContentView(R.layout.activity_main) addressAdapter = AddressAdapter() main_activity_recyclerView.adapter = addressAdapter } private fun respondToClicks() { main_activity_button.setOnClickListener({ showProgressBar() mMainViewModel.findAddress(main_activity_editText.text.toString()) }) addressAdapter setItemClickMethod { mMainViewModel.doOnItemClick(it) } } fun showProgressBar() { main_activity_progress_bar.visibility = View.VISIBLE } fun hideProgressBar() { main_activity_progress_bar.visibility = View.GONE } fun showErrorMessage(errorMsg: String) { Toast.makeText(this, "Error retrieving data: $errorMsg", Toast.LENGTH_SHORT).show() } override fun onStop() { super.onStop() mMainViewModel.cancelNetworkConnections() } fun updateMovieList(t: List<String>) { addressAdapter.updateList(t) addressAdapter.notifyDataSetChanged() } fun goToDetailActivity(item: MainModel.ResultEntity) { var bundle = Bundle() bundle.putString(DetailActivity.Constants.RATING, item.rating) bundle.putString(DetailActivity.Constants.TITLE, item.title) bundle.putString(DetailActivity.Constants.YEAR, item.year) bundle.putString(DetailActivity.Constants.DATE, item.date) var intent = Intent(this, DetailActivity::class.java) intent.putExtras(bundle) startActivity(intent) } class AddressAdapter : RecyclerView.Adapter<AddressAdapter.Holder>() { var mList: List<String> = arrayListOf() private lateinit var mOnClick: (position: Int) -> Unit override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder { val view = LayoutInflater.from(parent!!.context).inflate(R.layout.item, parent, false) return Holder(view) } override fun onBindViewHolder(holder: Holder, position: Int) { holder.itemView.item_textView.text = mList[position] holder.itemView.setOnClickListener { mOnClick(position) } } override fun getItemCount(): Int { return mList.size } infix fun setItemClickMethod(onClick: (position: Int) -> Unit) { this.mOnClick = onClick } fun updateList(list: List<String>) { mList = list } class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) } }

Aquí está el ViewModel:

class MainViewModel() { lateinit var resultListObservable: PublishSubject<List<String>> lateinit var resultListErrorObservable: PublishSubject<HttpException> lateinit var itemObservable: PublishSubject<MainModel.ResultEntity> private lateinit var entityList: List<MainModel.ResultEntity> private val compositeDisposable: CompositeDisposable = CompositeDisposable() private lateinit var mainModel: MainModel private val schedulersWrapper = SchedulersWrapper() constructor(mMainModel: MainModel) : this() { mainModel = mMainModel resultListObservable = PublishSubject.create() resultListErrorObservable = PublishSubject.create() itemObservable = PublishSubject.create() } fun findAddress(address: String) { val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() { override fun onSuccess(t: List<MainModel.ResultEntity>) { entityList = t resultListObservable.onNext(fetchItemTextFrom(t)) } override fun onError(e: Throwable) { resultListErrorObservable.onNext(e as HttpException) } }) compositeDisposable.add(disposable) } fun cancelNetworkConnections() { compositeDisposable.clear() } private fun fetchItemTextFrom(it: List<MainModel.ResultEntity>): ArrayList<String> { val li = arrayListOf<String>() for (resultEntity in it) { li.add("${resultEntity.year}: ${resultEntity.title}") } return li } fun doOnItemClick(position: Int) { itemObservable.onNext(entityList[position]) } }

y finalmente el Modelo:

class MainModel { private var mRetrofit: Retrofit? = null fun fetchAddress(address: String): Single<List<MainModel.ResultEntity>>? { return getRetrofit()?.create(MainModel.AddressService::class.java)?.fetchLocationFromServer(address) } private fun getRetrofit(): Retrofit? { if (mRetrofit == null) { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build() mRetrofit = Retrofit.Builder().baseUrl("http://bechdeltest.com/api/v1/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(client).build() } return mRetrofit } class ResultEntity(val title: String, val rating: String, val date: String, val year: String) interface AddressService { @GET("getMoviesByTitle") fun fetchLocationFromServer(@Query("title") title: String): Single<List<ResultEntity>> } }

Artículo completo here


En primer lugar, Android no te obliga a usar ninguna arquitectura. No solo eso, sino que también hace que sea algo difícil tratar de seguir a cualquiera. Esto requerirá que seas un desarrollador inteligente para evitar crear una base de código de spaghetti :)

Puedes intentar encajar en cualquier patrón que conozcas y te guste. Encuentro que el mejor enfoque de alguna manera te entrará en tus tripas a medida que desarrolles más y más aplicaciones (lo siento, pero como siempre, tendrás que cometer muchos errores hasta que comiences a hacerlo bien).

Sobre los patrones que conoces, déjame hacer algo mal: mezclaré tres patrones diferentes para que tengas la sensación de qué hace qué en Android. Creo que Presenter / ModelView debería estar en algún lugar del Fragmento o Actividad. Los adaptadores a veces pueden hacer este trabajo ya que se encargan de las entradas en las listas. Probablemente las actividades también deberían funcionar como controladores. Los modelos deben ser archivos java normales, mientras que la vista debe colocarse en los recursos de diseño y algunos componentes personalizados que puede tener que implementar.

Puedo darte algunos consejos. Esta es una respuesta wiki de la comunidad así que espero que otras personas puedan incluir otras sugerencias.

Organización de archivos

Creo que hay principalmente dos posibilidades sensatas:

  • organizar todo por tipo : cree una carpeta para todas las actividades, otra para todos los adaptadores, otra para todos los fragmentos, etc.
  • organizar todo por dominio (tal vez no sea la mejor palabra). Esto significa que todo lo relacionado con "ViewPost" estaría dentro de la misma carpeta: la actividad, el fragmento, los adaptadores, etc. Todo lo relacionado con "ViewPost" estaría en otra carpeta. Lo mismo para "EditPost", etc. Supongo que las actividades obligarían a las carpetas que crearías y luego habría algunas más genéricas para las clases base, por ejemplo.

Personalmente, solo he participado en proyectos usando el primer enfoque, pero realmente me gustaría probarlo más tarde, ya que creo que podría hacer las cosas más organizadas. No veo ninguna ventaja en tener una carpeta con 30 archivos no relacionados, pero eso es lo que obtengo con el primer enfoque.

Nombrando

  • Al crear diseños y estilos, siempre nombre (o identifíquelos) utilizando un prefijo para la actividad (/ fragmento) donde se utilizan.

Entonces, todas las cadenas, estilos, identificadores usados ​​en el contexto de "ViewPost" deberían comenzar siendo "@ id / view_post_heading" (para una vista de texto por ejemplo), "@ style / view_post_heading_style", "@ string / view_post_greeting".

Esto optimizará autocompletar, organización, evitar colisión de nombre, etc.

Clases base

Creo que querrás usar clases base para prácticamente todo lo que haces: adaptadores, actividades, fragmentos, servicios, etc. Estos pueden ser útiles al menos para fines de depuración para que sepas qué eventos están sucediendo en toda tu actividad.

General

  • Nunca uso clases anónimas: son feas y desviarán tu atención cuando intentes leer el código
  • A veces prefiero usar clases internas (en comparación con crear una clase dedicada): si una clase no se va a usar en ningún otro lugar (y es pequeña), creo que es muy útil.
  • Piense en su sistema de registro desde el principio: ¡puede usar el sistema de registro de Android pero hacer un buen uso de él!