web-dev-qa-db-de.com

Android-Anwendungsarchitektur - MVVM oder MVC?

Ich habe ein Android-Projekt, an dem ich gerade arbeite, und ich möchte, dass seine Struktur so robust wie möglich ist.

Ich komme aus einem WPF MVVM Hintergrund und ich habe ein wenig über Android Anwendungen Architektur gelesen, aber ich konnte einfach nicht eine gerade klare Antwort finden, die Architektur ich verwenden soll.

Einige Leute vorgeschlagen MVVM mit - http://vladnevzorov.com/2011/04/30/Android-application-architecture-part-ii-architectural-styles-and-patterns/

und andere vorgeschlagen MVC verwenden, aber nicht angeben, wie genau sollte sie umgesetzt werden.

Wie ich schon sagte, ich komme aus einem WPF-MVVM Hintergrund, und deshalb weiß ich es auf Bindungen stark stützt, das so weit wie ich es verstehe, ist standardmäßig nicht in Android unterstützt.

Es scheint, wie es eine 3rd-Party-Lösung ist - http://code.google.com/p/Android-binding/ Aber ich weiß nicht, ob ich darauf verlassen mag. Was wäre, wenn seine Entwicklung aufhören würde und es nicht mehr von zukünftigen APIs und so weiter unterstützt wird?.

Im Grunde, was ich suche, ist eine ausführliche Anleitung, die mir die Best Practices für den Aufbau der Anwendung der Struktur lehren. Ordner- und Klassenstruktur usw. Ich konnte einfach kein gründliches Tutorial finden, und ich hätte erwartet, dass Google seinen Entwicklern ein solches Tutorial zur Verfügung stellt. Ich denke nur nicht, dass diese Art von Dokumentation den technischen Aspekt gut genug behandelt - http://developer.Android.com/guide/topics/fundamentals.html

Ich hoffe, dass ich klar genug bin und nicht zu viel verlange. Ich möchte nur sicher sein, dass die Struktur meiner Anwendung stimmt, bevor sich mein Code in ein Spaghetti-Monster verwandelt.

Vielen Dank!

35
Dror

Zunächst zwingt Android Sie nicht, eine Architektur zu verwenden. Nicht nur das, es macht es auch etwas schwierig zu folgen. Dies erfordert, dass Sie ein intelligenter Entwickler sind, um die Erstellung einer Spaghetti-Codebasis zu vermeiden :)

Sie können versuchen, in jedes Muster zu passen, das Sie kennen und das Sie mögen. Ich finde, dass der beste Ansatz in gewisser Weise in den Bauch geht, wenn Sie mehr und mehr Anwendungen entwickeln (tut mir leid, aber wie immer müssen Sie viele Fehler machen, bis Sie es richtig machen).

Über die Muster, die Sie kennen, lassen Sie mich etwas falsch machen: Ich werde drei verschiedene Muster mischen, damit Sie das Gefühl bekommen, was in Android funktioniert. Ich glaube, der Presenter/ModelView sollte sich irgendwo im Fragment oder in der Aktivität befinden. Manchmal erledigen Adapter diese Aufgabe, da sie sich um Eingaben in Listen kümmern. Wahrscheinlich sollten Aktivitäten auch wie Controller funktionieren. Modelle sollten normale Java-Dateien sein, während die Ansicht Layoutressourcen und einige benutzerdefinierte Komponenten enthalten sollte, die Sie möglicherweise implementieren müssen.


Ich kann dir ein paar Tipps geben. Dies ist eine Community-Wiki-Antwort so können andere Leute hoffentlich andere Vorschläge machen.

Dateiorganisation

Ich denke, es gibt hauptsächlich zwei sinnvolle Möglichkeiten:

  • organisieren Sie alles nach type - Erstellen Sie einen Ordner für alle Aktivitäten, einen anderen Ordner für alle Adapter, einen anderen Ordner für alle Fragmente usw
  • organisiere alles nach domain (vielleicht nicht das beste Word). Dies würde bedeuten, dass sich alles, was mit "ViewPost" zusammenhängt, im selben Ordner befinden würde - die Aktivität, das Fragment, die Adapter usw. Alles, was mit "ViewPost" zusammenhängt, befindet sich in einem anderen Ordner. Gleiches gilt für "EditPost" usw. Ich denke, Aktivitäten würden die von Ihnen erstellten Ordner vorschreiben, und dann würde es ein paar mehr generische für Basisklassen geben.

Persönlich war ich nur an Projekten mit dem ersten Ansatz beteiligt, aber ich würde es gerne später versuchen, da ich glaube, es könnte die Dinge besser organisieren. Ich sehe keinen Vorteil darin, einen Ordner mit 30 nicht zusammenhängenden Dateien zu haben, aber das ist es, was ich mit dem ersten Ansatz bekomme.

Benennung

  • Benennen Sie bei der Erstellung von Layouts und Stilen immer ein Präfix für die Aktivität (/ Fragment), in der sie verwendet werden.

Daher sollten alle Zeichenketten, Stile und IDs, die im Kontext von "ViewPost" verwendet werden, "@ id/view_post_heading" (beispielsweise für eine Textansicht), "@ style/view_post_heading_style", "@ string/view_post_greeting" sein.

Dadurch werden Autovervollständigung, Organisation optimiert, Namenskollisionen vermieden usw.

Basisklassen

Ich denke, Sie möchten Basisklassen für so ziemlich alles, was Sie tun: Adapter, Aktivitäten, Fragmente, Dienste usw., verwenden. Diese können zumindest für Debugging-Zwecke nützlich sein, damit Sie wissen, welche Ereignisse in Ihrer gesamten Aktivität stattfinden.

Allgemeines

  • Ich verwende niemals anonyme Klassen - diese sind hässlich und werden Ihre Aufmerksamkeit ablenken, wenn Sie versuchen, den Code zu lesen
  • Manchmal ziehe ich die Verwendung von inneren Klassen vor (verglichen mit dem Erstellen einer dedizierten Klasse). Wenn eine Klasse nirgendwo anders verwendet wird (und sie ist klein), finde ich das sehr praktisch.
  • Denken Sie von Anfang an über Ihr Protokolliersystem nach - Sie können das Protokolliersystem von Android verwenden, aber es gut nutzen!
34
Pedro Loureiro

Ich denke, es wäre hilfreicher, MVVM in Android anhand eines Beispiels zu erklären. Der vollständige Artikel zusammen mit der GitHub-Repo-Info ist hier für weitere Informationen.

Nehmen wir an, dass das gleiche Beispiel für eine Benchmark-Film-App im ersten Teil dieser Serie eingeführt wurde. Der Benutzer gibt einen Suchbegriff für einen Film ein und drückt die Taste "FIND". Darauf basierend sucht die App nach der Liste der Filme einschließlich des Suchbegriffs und zeigt sie an. Wenn Sie auf jeden Film in der Liste klicken, werden die Details angezeigt.

 enter image description here

Ich werde jetzt erklären, wie diese App in MVVM implementiert wird, gefolgt von der vollständigen Android-App, die auf meiner GitHub-Seite verfügbar ist.

Wenn der Benutzer in der Ansicht auf die Schaltfläche "FIND" klickt, wird eine Methode aus dem ViewModel mit dem Suchbegriff als Argument aufgerufen:

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

Das ViewModel ruft dann die findAddress-Methode aus dem Model auf, um nach dem Filmnamen zu suchen:

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

Wenn die Antwort vom Modell stammt, trägt die onSuccess-Methode des RxJava-Observers das erfolgreiche Ergebnis. Da das ViewModel jedoch View-agnostisch ist, hat es keine View-Instanz, um das Ergebnis zur Anzeige zu übergeben. Es löst stattdessen ein Ereignis in der resultListObservable aus, indem es resultListObservable.onNext (fetchItemTextFrom (t)) aufruft, das von der View beobachtet wird:

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

Das Observable spielt also eine Vermittlerrolle zwischen View und ViewModel:

  • ViewModel löst ein Ereignis in seiner Beobachtbarkeit aus 
  • View aktualisiert die Benutzeroberfläche, indem es das beobachtbare ViewModel abonniert

Hier ist der vollständige Code für die Ansicht. In diesem Beispiel ist View eine Aktivitätsklasse, Fragment kann jedoch auch gleichermaßen verwendet werden:

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

}

Hier ist das 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])
    }
}

und schließlich das Model:

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

}

Ganzer Artikel hier

0
Ali Nem