Android应用程序架构——MVVM还是MVC?

发布于 2024-12-21 06:27:37 字数 1020 浏览 3 评论 0原文

我有一个正在开始处理的 android 项目,我希望它的结构尽可能健壮。

我有 WPF MVVM 背景,并且阅读了一些有关 Android 应用程序架构的内容,但我找不到关于我应该使用哪种架构的直接明确的答案。

有些人建议使用 MVVM - http://vladnevzorov.com/2011/04/30/android-application-architecture-part-ii-architectural-styles-and-patterns/

和其他人建议使用 MVC,但没有没有具体说明应该如何实施。

正如我所说,我来自 WPF-MVVM 背景,因此我知道它严重依赖于绑定,据我所知,Android 默认情况下不支持绑定。

似乎有一个第三方解决方案 - http://code.google.com/p/ android-绑定/ 但我不知道我是否愿意依赖它。如果它的开发停止并且未来的 API 等不支持它怎么办?

基本上我正在寻找的是一个完整的教程,它将教我构建应用程序结构的最佳实践。文件夹和类结构等。我只是找不到任何完整的教程,我希望谷歌会为其开发人员提供这样的教程。我只是认为这种文档在技术方面处理得不够好 - http:// /developer.android.com/guide/topics/fundamentals.html

我希望我已经足够清楚了,我没有要求太多,我只是想在我的应用程序之前确定我的应用程序的结构代码会变成意大利面条怪物。

谢谢!

I got an android project I'm beginning to work on, and I want its structure to be as robust as possible.

I'm coming from a WPF MVVM background and I've been reading a little about android applications architecture, but I just couldn't find a straight clear answer about which architecture I should use.

Some people suggested using MVVM - http://vladnevzorov.com/2011/04/30/android-application-architecture-part-ii-architectural-styles-and-patterns/

and others suggested using MVC, but didn't specify how exactly it should be implemented.

As I said I'm coming from a WPF-MVVM background, and therefore I know it heavily relies on bindings which as far as I understand, are not supported by default in Android.

It seems like there is a 3rd party solution - http://code.google.com/p/android-binding/
But I don't know if I'd like to rely on that. What if its development would stop and it will not be supported by future APIs and etc..

Basically what I'm looking for is a thorough tutorial that will teach me the best practices for building the application's structure. Folders and classes structure and etc. I just couldn't find any thorough tutorial, and I would have expected that Google would supply such a tutorial for its developers. I just don't think that this kind of documentation handles the technical aspect good enough - http://developer.android.com/guide/topics/fundamentals.html

I hope I've been clear enough and that I'm not asking for too much, I just want to be sure about my application's structure, before my code will turn into a spaghetti monster.

Thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

假装爱人 2024-12-28 06:27:37

首先,Android 并不强迫你使用任何架构。不仅如此,它还使得尝试遵循任何内容都有些困难。这将要求您成为一名聪明的开发人员,以避免创建意大利面条式的代码库:)

您可以尝试适应您知道和喜欢的任何模式。我发现,当您开发越来越多的应用程序时,最好的方法会以某种方式进入您的内心(对此感到抱歉,但一如既往,您将不得不犯很多错误,直到您开始做正确的事情)。

关于您所知道的模式,让我做一些错误的事情:我将混合三种不同的模式,以便您了解 Android 中的功能。我相信 Presenter/ModelView 应该位于 Fragment 或 Activity 中的某个位置。适配器有时可能会完成这项工作,因为它们处理列表中的输入。也许活动也应该像控制器一样工作。模型应该是常规的 java 文件,而视图应该位于布局资源和一些您可能必须实现的自定义组件中。


我可以给你一些建议。 这是一个社区 wiki 答案,因此希望其他人可能会提出其他建议。

文件组织

我认为主要有两种合理的可能性:

  • 类型组织所有内容 - 为所有活动创建一个文件夹,为所有适配器创建另一个文件夹,为所有片段创建另一个文件夹,等等
  • 域组织所有内容(也许不是最好的词)。这意味着与“ViewPost”相关的所有内容都将位于同一文件夹中 - 活动、片段、适配器等。与“ViewPost”相关的所有内容都将位于另一个文件夹中。对于“EditPost”等也是如此。我想活动会强制您创建的文件夹,然后会有一些更通用的基类文件夹。

就我个人而言,我只参与过使用第一种方法的项目,但我真的很想尝试第二种方法,因为我相信它可以使事情更有条理。我认为拥有一个包含 30 个不相关文件的文件夹没有任何优势,但这就是我使用第一种方法得到的结果。

命名

  • 创建布局和样式时,始终使用使用它们的活动 (/fragment) 的前缀来命名(或标识它们)。

因此,在“ViewPost”上下文中使用的所有字符串、样式、id 都应以“@id/view_post_heading”(例如,对于文本视图)、“@style/view_post_heading_style”、“@string/view_post_greeting”开头。

这将优化自动完成、组织、避免名称冲突等。

基类

我认为您将希望使用基类来完成您所做的几乎所有事情:适配器、活动、片段、服务等。这些至少对于调试可能有用目的,以便您了解所有活动中正在发生哪些事件。

一般

  • 我从不使用匿名类 - 这些很丑陋,当你尝试阅读代码时会分散你的注意力
  • 有时我更喜欢使用内部类(与创建专用类相比) - 如果一个类不会在任何地方使用否则(而且它很小)我认为这非常方便。
  • 从一开始就考虑一下您的日志系统 - 您可以使用 Android 的日志系统,但要充分利用它!

First of all, Android doesn't force you to use any architecture. Not only that but it also makes it somewhat difficult to try to follow to any. This will require you to be a smart developer in order to avoid creating a spaghetti codebase :)

You can try to fit in any pattern you know and you like. I find that the best approach will in some way get into your guts as you develop more and more applications (sorry about that but as always, you'll have to make lots of mistakes until you start doing it right).

About the patterns you know, let me do something wrong: I'll mix three different patterns so you get the feeling of what does what in android. I believe the Presenter/ModelView should be somewhere in the Fragment or Activity. Adapters might sometimes do this job as they take care of inputs in lists. Probably Activities should work like Controllers too. Models should be regular java files whereas the View should lay in layout resources and some custom components you might have to implement.


I can give you some tips. This is a community wiki answer so hopefully other people might include other suggestions.

File Organization

I think there are mainly two sensible possibilities:

  • organize everything by type - create a folder for all activities, another folder for all adapters, another folder for all fragments, etc
  • organize everything by domain (maybe not the best word). This would mean everything related to "ViewPost" would be inside the same folder - the activity, the fragment, the adapters, etc. Everything related to "ViewPost" would be in another folder. Same for "EditPost", etc. I guess activities would mandate the folders you'd create and then there would be a few more generic ones for base classes for example.

Personally, I have only been involved in projects using the first approach but I really would like to try the later as I believe it could make things more organized. I see no advantage in having a folder with 30 unrelated files but that's what I get with the first approach.

Naming

  • When creating layouts and styles, always name (or identify them) using a prefix for the activity (/fragment) where they are used.

So, all strings, styles, ids used in the context of "ViewPost" should start be "@id/view_post_heading" (for a textview for example), "@style/view_post_heading_style", "@string/view_post_greeting".

This will optimize autocomplete, organization, avoid name colision, etc.

Base Classes

I think you'll want to use base classes for pretty much everything you do: Adapters, Activities, Fragments, Services, etc. These might be useful at least for debugging purposes so you know which events are happening in all your activity.

General

  • I never use anonymous classes - these are ugly and will drive your attention away when you are trying to read the code
  • Sometimes I prefer to use inner classes (compared to create a dedicated class) - if a class is not going to be used anywhere else (and it's small) I think this is very handy.
  • Think about your logging system from the beginning - you can use android's logging system but make a good use of it!
寄居者 2024-12-28 06:27:37

我认为通过一个例子来解释android中的MVVM会更有帮助。完整的文章以及 GitHub 存储库信息位于 此处< /a> 了解更多信息。

让我们假设本系列第一部分中介绍的相同基准电影应用程序示例。用户输入电影的搜索词并按“查找”按钮,应用程序将根据该按钮搜索包含该搜索词的电影列表并显示它们。单击列表中的每部电影即可显示其详细信息。

输入图片这里的描述

我现在将解释如何在 MVVM 中实现此应用程序,然后是完整的 Android 应用程序,该应用程序可在 我的 GitHub 页面

当用户单击视图上的“FIND”按钮时,会从 ViewModel 中调用一个方法,并将搜索词作为其参数:

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

然后,ViewModel 从 Model 中调用 findAddress 方法来搜索电影名称:

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

当响应来自 Model 时,RxJava 观察者的 onSuccess 方法会携带成功结果,但由于 ViewModel 与 View 无关,因此它没有或不使用任何 View 实例来传递显示结果。相反,它通过调用 resultListObservable.onNext(fetchItemTextFrom(t)) 来触发 resultListObservable 中的事件,该事件由 View 观察:

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

因此 observable 在 View 和 ViewModel 之间扮演中介角色:

  • ViewModel 触发其 observable 中的事件
  • View 更新通过订阅 ViewModel 的 observable 来实现 UI

以下是 View 的完整代码。在此示例中,View 是一个 Activity 类,但 Fragment 也可以同样使用:

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

}

这是 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])
    }
}

最后是 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>>
    }

}

全文 此处

I think it would be more helpful to explain MVVM in android through an example. The complete article together with the GitHub repo info is here for more info.

Let’s suppose the same benchmark movie app example introduced in the first part of this series. User enters a search term for a movie and presses the ‘FIND’ button, based on which the app searches for the list of movies including that search term and shows them. Clicking on each movie on the list then shows its details.

enter image description here

I will now explain how this app is implemented in MVVM followed by the complete android app, which is available on my GitHub page.

When the user clicks on the ‘FIND’ button on the View, a method is called from the ViewModel with the search term as its argument:

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

The ViewModel then calls the findAddress method from the Model to search for the movie name:

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

When the response comes from the Model, the onSuccess method of the RxJava observer carries the successful result, but as the ViewModel is View agnostic, it does not have or use any View instance to pass the result for showing. It instead triggers an event in the resultListObservable by calling resultListObservable.onNext(fetchItemTextFrom(t)) , which is observed by the View:

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

So the observable plays a mediator role between the View and ViewModel:

  • ViewModel triggers an event in its observable
  • View updates the UI by subscribing to ViewModel’s observable

Here’s the full code for the View. In this example, View is an Activity class, but Fragment can also be equally used:

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

}

Here is the 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])
    }
}

and finally the 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>>
    }

}

Full article here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文