web-dev-qa-db-fra.com

Architecture d'application Android - MVVM ou MVC?

Je commence à travailler sur un projet Android et je souhaite que sa structure soit aussi robuste que possible.

Je viens d'un contexte WPVM MVVM et je lisais un peu sur l'architecture d'applications Android, mais je ne trouvais pas de réponse claire quant à l'architecture à utiliser.

Certaines personnes ont suggéré d'utiliser MVVM - http://vladnevzorov.com/2011/04/30/Android-application-architecture-part-ii-architectural-styles-and-patterns/

et d'autres ont suggéré d'utiliser MVC, mais n'ont pas précisé comment exactement cela devrait être mis en œuvre.

Comme je l'ai dit, je viens d'un contexte WPF-MVVM et, par conséquent, je sais qu'il est fortement tributaire de liaisons qui, pour autant que je sache, ne sont pas prises en charge par défaut dans Android.

Il semble qu’il existe une solution tierce - http://code.google.com/p/Android-binding/ Mais je ne sais pas si je voudrais compter sur cela. Et si son développement s'arrêtait et qu'il ne serait pas pris en charge par les futures API, etc.

En gros, ce que je recherche, c’est un tutoriel complet qui m’apprendra les meilleures pratiques pour la construction de la structure de l’application. Structure des dossiers et des classes, etc. Je ne pouvais tout simplement pas trouver de didacticiel complet, et je me serais attendu à ce que Google fournisse un tel didacticiel à ses développeurs. Je ne pense tout simplement pas que ce type de documentation traite suffisamment l’aspect technique - http://developer.Android.com/guide/topics/fundamentals.html

J'espère que j'ai été suffisamment clair et que je ne demande pas trop, je veux juste être sûr de la structure de mon application, avant que mon code ne se transforme en un monstre de spaghettis.

Merci!

35
Dror

Tout d'abord, Android ne vous oblige à utiliser aucune architecture. Non seulement cela, mais cela rend également quelque peu difficile d'essayer de suivre à tout. Cela vous obligera à être un développeur intelligent afin d'éviter de créer une base de code spaghetti :)

Vous pouvez essayer de vous adapter à n'importe quel modèle que vous connaissez et que vous aimez. Je trouve que la meilleure approche vous intéressera d’une manière ou d’une autre au fur et à mesure que vous développerez de plus en plus d’applications (désolé, mais vous devrez faire beaucoup d’erreurs jusqu’à ce que vous commenciez à bien faire les choses).

En ce qui concerne les modèles que vous connaissez, laissez-moi faire quelque chose de mal: je vais mélanger trois modèles différents pour vous donner une idée de ce qui se passe dans Android. Je crois que le présentateur/ModelView devrait être quelque part dans le fragment ou l'activité. Les adaptateurs peuvent parfois faire ce travail car ils s’occupent des entrées dans les listes. Probablement les activités devraient fonctionner comme des contrôleurs aussi. Les modèles doivent être des fichiers Java normaux, tandis que la vue doit figurer dans les ressources de présentation et dans certains composants personnalisés à implémenter.


Je peux vous donner quelques conseils. Ceci est une réponse du wiki de la communauté donc j'espère que d'autres personnes pourront inclure d'autres suggestions.

Organisation du fichier

Je pense qu'il y a principalement deux possibilités sensibles:

  • tout organiser par type - créer un dossier pour toutes les activités, un autre dossier pour tous les adaptateurs, un autre dossier pour tous les fragments, etc.
  • tout organiser par domain (peut-être pas le meilleur mot). Cela signifierait que tout ce qui est lié à "ViewPost" se trouverait dans le même dossier - l'activité, le fragment, les adaptateurs, etc. Tout ce qui était lié à "ViewPost" se trouverait dans un autre dossier. Pareil pour "EditPost", etc. J'imagine que les activités exigeraient les dossiers que vous créeriez, puis quelques autres plus génériques pour les classes de base, par exemple.

Personnellement, je n'ai été impliqué que dans des projets utilisant la première approche, mais j'aimerais vraiment essayer plus tard, car je pense que cela pourrait rendre les choses plus organisées. Je ne vois aucun avantage à avoir un dossier avec 30 fichiers sans rapport, mais c'est ce que j'ai avec la première approche.

Appellation

  • Lors de la création de modèles et de styles, nommez-les (ou identifiez-les) en utilisant un préfixe pour l'activité (/ fragment) où ils sont utilisés.

Ainsi, toutes les chaînes, styles et identifiants utilisés dans le contexte de "ViewPost" doivent commencer par "@ id/view_post_heading" (pour une vue de texte par exemple), "@ style/view_post_heading_style", "@ string/view_post_greeting".

Cela optimisera la saisie semi-automatique, l'organisation, évitera la collision de noms, etc.

Classes de base

Je pense que vous voudrez utiliser les classes de base pour pratiquement tout ce que vous faites: adaptateurs, activités, fragments, services, etc. Cela peut être utile au moins à des fins de débogage afin que vous sachiez quels événements se produisent dans l'ensemble de votre activité.

Général

  • Je n'utilise jamais de classes anonymes - elles sont laides et attireront votre attention lorsque vous essayez de lire le code
  • Parfois, je préfère utiliser des classes internes (par rapport à la création d'une classe dédiée) - si une classe ne sera utilisée nulle part ailleurs (et que c'est petit), je pense que c'est très pratique.
  • Pensez à votre système de journalisation depuis le début - vous pouvez utiliser le système de journalisation d'Android mais faites-en bon usage!
34
Pedro Loureiro

Je pense qu'il serait plus utile d'expliquer MVVM dans Android à travers un exemple. L'article complet avec les informations de dépôt GitHub est ici pour plus d'informations.

Supposons le même exemple d’application de film de référence que celui présenté dans la première partie de cette série. L’utilisateur entre un terme de recherche pour un film et appuie sur le bouton «RECHERCHER», sur lequel l’application recherche la liste de films comprenant ce terme de recherche et les affiche. Cliquez sur chaque film de la liste pour afficher ses détails.

 enter image description here

Je vais maintenant expliquer comment cette application est mise en œuvre dans MVVM, suivie de l'application complète Android, disponible sur ma page GitHub .

Lorsque l’utilisateur clique sur le bouton ‘RECHERCHER’ de la vue, une méthode est appelée à partir du ViewModel avec le terme recherché comme argument:

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

ViewModel appelle ensuite la méthode findAddress à partir du modèle pour rechercher le nom du film:

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)
}

Lorsque la réponse provient du modèle, la méthode onSuccess de l'observateur RxJava porte le résultat positif, mais comme ViewModel est agnostique sur View, il ne possède ni n'utilise aucune instance de View pour transmettre le résultat. À la place, il déclenche un événement dans resultListObservable en appelant resultListObservable.onNext (fetchItemTextFrom (t)), qui est observé par la vue:

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

L'observable joue donc un rôle de médiateur entre View et ViewModel:

  • ViewModel déclenche un événement dans son observable 
  • View met à jour l'interface utilisateur en souscrivant à l'observable de ViewModel.

Voici le code complet de la vue. Dans cet exemple, View est une classe d'activité, mais Fragment peut également être utilisé de la même manière:

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)
    }

}

Voici le 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])
    }
}

et enfin le modèle:

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>>
    }

}

Article complet ici

0
Ali Nem