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!
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.
Je pense qu'il y a principalement deux possibilités sensibles:
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.
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.
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é.
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.
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:
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