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

35

我开始着手一个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-binding/但我不知道是否要依赖它。如果它的开发停止,并且不受未来API等的支持,怎么办。

基本上,我正在寻找一份详尽的教程,教我构建应用程序结构的最佳实践。文件夹和类结构等。我找不到任何详细的教程,我本来希望Google会为开发人员提供这样的教程。我只是认为这种文档没有很好地处理技术方面-http://developer.android.com/guide/topics/fundamentals.html

我希望我已经表达清楚,并且我没有要求过多,我只想确保我的应用程序结构在代码变成意大利面条怪物之前得到保证。

谢谢!


4
非常好的问题,你可能认为谷歌或者任何人都会有一些关于这个主题的全面教程或书籍。我正处于同样的困境;虽然有MVC框架的Web开发背景,但在Android中实现一个统一的应用架构仍然使我头疼。 - Brian Lacy
4
我开始了android-binding项目(http://code.google.com/p/android-binding/)。我认为我正在努力地保持开发它的势头:)现在我们已经支持ICS。 - xandy
1
在这里查看我的博客文章 Android 架构:MV? - Dori
2个回答

36
  • 首先,Android不会强制要求你使用任何架构。更不止于此,它还使得尝试遵循任何架构有些困难。这将需要您成为一位聪明的开发人员,以避免创建意大利面代码库 :)

  • 您可以尝试适应任何您知道和喜欢的模式。我发现最好的方法是在您开发越来越多的应用程序时会某种程度地深入你的内心(对此我很抱歉,但总是要犯很多错误才能做正确)。

  • 关于您知道的模式,让我做错事情:我将混合三种不同的模式,以便您了解在Android中哪个模式做什么。我认为Presenter / ModelView应该在Fragment或Activity中的某个位置。适配器有时也会完成此工作,因为它们负责列表中的输入。可能活动也应该像控制器一样工作。模型应该是常规的java文件,而视图应该放置在布局资源和您可能需要实现的某些自定义组件中。


  • 我可以给您提供一些提示。这是一个社区维基答案,希望其他人能提供其他建议。

  • 文件组织

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

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

    就个人而言,我只参与过使用第一种方法的项目,但我真的很想尝试后者,因为我认为它可以使事情更有组织。我看不出拥有30个无关文件的文件夹的优点。

    命名

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

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

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

    基础类

    我认为你几乎想要为你所做的一切使用基础类:适配器、活动、片段、服务等。这些至少对于调试目的可能很有用,这样你就知道所有活动中发生了哪些事件。

    通用

    • 我从不使用匿名类——它们很丑陋,会分散你阅读代码的注意力。
    • 有时我更喜欢使用内部类(而不是创建专门的类)——如果一个类不会在任何其他地方使用(而且它很小),我认为这非常方便。
    • 从一开始就考虑好你的日志记录系统——你可以使用Android的日志记录系统,但要好好利用它!

    0

    我认为通过一个例子来解释在Android中的MVVM会更有帮助。完整的文章以及GitHub存储库信息可以在这里找到。

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

    enter image description here

    我现在将解释这个应用程序是如何使用MVVM实现的,然后是完整的Android应用程序,可在我的GitHub页面上找到。

    当用户在视图上点击“查找”按钮时,将从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)
    })
    

    因此,可观察对象在视图和视图模型之间扮演中介者的角色:

    • 视图模型在其可观察对象中触发事件
    • 视图通过订阅视图模型的可观察对象来更新UI

    这是视图的完整代码。在此示例中,视图是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])
        }
    }
    

    最后是模型:

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

    完整文章在这里


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接