写点什么

我是怎么把业务代码越写越复杂的 _ MVP - MVVM - Clean Architecture

用户头像
Android架构
关注
发布于: 2021 年 11 月 03 日

GodActivity 引入了大量本和它无关的类:Retrofit、Executors、ContentValues、Cursor、SQLiteDatabase、Response、OkHttpClient。Activity 本应该只和界面展示有关。

将界面展示和获取数据分离

既然 Activity 知道太多,那就让 Presenter 来为它分担:


// 构造 Presenter 时传入 view 层接口 NewsViewclass NewsPresenter(var newsView: NewsView): NewsBusiness {private val retrofit = Retrofit.Builder().baseUrl("https://api.apiopen.top").addConverterFactory(MoshiConverterFactory.create()).client(OkHttpClient.Builder().build()).build()


private val newsApi = retrofit.create(NewsApi::class.java)


private var executor = Executors.newSingleThreadExecutor()


override fun fetchNews() {// 将数据库新闻通过 view 层接口通知 ActivityqueryNews().let{ newsView.showNews(it) }newsApi.fetchNews(mapOf("page" to "1", "count" to "4")).enqueue(object : Callback<NewsBean> {override fun onFailure(call: Call<NewsBean>, t: Throwable) {newsView.showNews(null)}


override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {response.body()?.result?.let {// 将网络新闻通过 view 层接口通知 ActivitynewsView.showNews(it)dbExecutor.submit { insertNews(it) }}}})}


// 从数据库读老新闻(伪代码)private fun queryNews() : List<News> {// 通过 view 层接口获取 context 构造 dbHelperval dbHelper = NewsDbHelper(newsView.newsContext, ...)val db = dbHelper.getReadableDatabase()val cursor = db.query(...)var newsList = mutableListOf<News>()while(cursor.moveToNext()) {...newsList.add(news)}db.close()return newsList}


// 将新闻写入数据库(伪代码)private fun insertNews(news : List<News>) {val dbHelper = NewsDbHelper(newsView.newsContext, ...)val db = dbHelper.getWriteableDatabase()news.foreach {val cv = ContentValues().apply { ... }db.insert(cv)}db.close()}}


无非就是复制 + 粘贴,把 GodActivity 中的“异步”、“访问数据库”、“访问网络”、放到了一个新的 Presenter 类中。这样 Activity 就变简单了:


class RetrofitActivity : AppCompatActivity(), NewsView {// 在界面中直接构造业务接口实例 private val newsBusiness =


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


NewsPresenter(this)


private var rvNews: RecyclerView? = nullprivate var newsAdapter = NewsAdapter()


override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.news_activity)initView()// 触发业务逻辑 newsBusiness.fetchNews()}


private fun initView() {rvNews = findViewById(R.id.rvNews)rvNews?.layoutManager = LinearLayoutManager(this)}


// 实现 View 层接口以更新界面 override fun showNews(news: List<News>?) {newsAdapter.news = newsrvNews?.adapter = newsAdapter}


override val newsContext: Contextget() = this}


Presenter 的引入还增加了通信成本:


interface NewsBusiness {fun fetchNews()}


这是 MVP 模型中的业务接口,描述的是业务动作。它由Presenter实现,而界面类持有它以触发业务逻辑。


interface NewsView {// 将新闻传递给界面 fun showNews(news:List<News>?)// 获取界面上下文 abstract val newsContext:Context}


在 MVP 模型中,这称为 View 层接口Presenter持有它以触发界面更新,而界面类实现它以绘制界面。


这两个接口的引入,意义非凡:


接口把 做什么(抽象) 和 怎么做(细节) 分离。这个特性使得 关注点分离 成为可能:接口持有者只关心 做什么,而 怎么做 留给接口实现者关心。


Activity 持有业务接口,这使得它不需要关心业务逻辑的实现细节。Activity 实现 View 层接口,界面展示细节都内聚在 Activity 类中,使其成为MVP中的V


Presenter 持有 View 层接口,这使得它不需要关心界面展示细节。Presenter 实现业务接口,业务逻辑的实现细节都内聚在 Presenter 类中,使其成为MVP中的P


这样做最大的好处是降低代码理解成本,因为不同细节不再是在同一层次被铺开,而是被分层了。阅读代码时,“浅尝辄止”或“不求甚解”的阅读方式极大的提高了效率。


这样做还能缩小变更成本,业务需求发生变更时,只有 Presenter 类需要改动。界面调整时,只有V层需要改动。同理,排查问题的范围也被缩小。


这样还方便了自测,如果想测试各种临界数据产生时界面的表现,则可以实现一个PresenterForTest。如果想覆盖业务逻辑的各种条件分支,则可以方便地给 Presenter 写单元测试(和界面隔离后,Presenter 是纯 Kotlin 的,不含有任何 Android 代码)。


NewsPresenter也不单纯!它除了包含业务逻辑,还包含了访问数据的细节,应该用同样的思路,抽象出一个访问数据的接口,让 Presenter 持有,这就是MVP中的M。它的实现方式可以参考下一节的Repository

数据视图互绑 + 长生命周期数据

即使将访问数据的细节剥离出 Presenter,它依然不单纯。因为它持有 View 层接口,这就要求Presenter需了解 该把哪个数据传递给哪个接口方法,这就是 数据绑定,它在构建视图时就已经确定(无需等到数据返回),所以这个细节可以从业务层剥离,归并到视图层。


Presenter的实例被 Activity 持有,所以它的生命周期和 Activiy 同步,即业务数据和界面同生命周期。在某些场景下,这是一个缺点,比如横竖屏切换。此时,如果数据的生命周期不依赖界面,就可以免去重新获取数据的成本。这势必 需要一个生命周期更长的对象(ViewModel)持有数据。

生命周期更长的 ViewModel

上一节的例子中,构建 Presenter 是直接在 Activity 中 new,而构建ViewModel是通过ViewModelProvider.get():


public class ViewModelProvider {// ViewModel 实例商店 private final ViewModelStore mViewModelStore;


public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {// 从商店获取 ViewModel 实例 ViewModel viewModel = mViewModelStore.get(key);


if (modelClass.isInstance(viewModel)) {return (T) viewModel;} else {...}// 若商店无 ViewModel 实例 则通过 Factory 构建 if (mFactory instanceof KeyedFactory) {viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);} else {viewModel = (mFactory).create(modelClass);}// 将 ViewModel 实例存入商店 mViewModelStore.put(key, viewModel);return (T) viewModel;}}


ViewModel实例通过ViewModelStore获取:


// ViewModel 实例商店 public class ViewModelStore {// 存储 ViewModel 实例的 Mapprivate final HashMap<String, ViewModel> mMap = new HashMap<>();


// 存 final void put(String key, ViewModel viewModel) {ViewModel oldViewModel = mMap.put(key, viewModel);if (oldViewModel != null) {oldViewModel.onCleared();}}


// 取 final ViewModel get(String key) {return mMap.get(key);}


...}


ViewModelStoreViewModel实例存储在HashMap中。


ViewModelStore通过ViewModelStoreOwner获取:


public class ViewModelProvider {// ViewModel 实例商店 private final ViewModelStore mViewModelStore;


// 构造 ViewModelProvider 时需传入 ViewModelStoreOwner 实例 public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {// 通过 ViewModelStoreOwner 获取 ViewModelStorethis(owner.getViewModelStore(), factory);}


public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {mFactory = factory;mViewModelStore = store;}}


ViewModelStoreOwner实例又存储在哪?


// Activity 基类实现了 ViewModelStoreOwner 接口 public class ComponentActivity extends androidx.core.app.ComponentActivity implementsLifecycleOwner,ViewModelStoreOwner,SavedStateRegistryOwner,OnBackPressedDispatcherOwner {


// Activity 持有 ViewModelStore 实例 private ViewModelStore mViewModelStore;


public ViewModelStore getViewModelStore() {if (mViewModelStore == null) {// 获取配置无关实例 NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {// 从配置无关实例中恢复 ViewModel 商店 mViewModelStore = nc.viewModelStore;}if (mViewModelStore == null) {mViewModelStore = new ViewModelStore();}}return mViewModelStore;}


// 静态的配置无关实例 static final class NonConfigurationInstances {// 持有 ViewModel 商店实例 ViewModelStore viewModelStore;...}}


Activity 就是ViewModelStoreOwner实例,且持有ViewModelStore实例,该实例还会被保存在一个静态类中,所以 ViewModel 生命周期比 Activity 更长。这样 ViewModel 中存放的业务数据就可以在 Activity 销毁重建时被复用。

数据绑定

MVVM 中 Activity 属于 V 层,布局构建以及数据绑定都在这层完成:


class MvvmActivity : AppCompatActivity() {private var rvNews: RecyclerView? = nullprivate var newsAdapter = NewsAdapter()


// 构建布局 private val rootView by lazy {ConstraintLayout {TextView {layout_id = "tvTitle"layout_width = wrap_contentlayout_height = wrap_contenttextSize = 25fpadding_start = 20padding_end = 20center_horizontal = truetext = "News"top_toTopOf = parent_id}


rvNews = RecyclerView {layout_id = "rvNews"layout_width = match_parentlayout_height = wrap_contenttop_toBottomOf = "tvTitle"margin_top = 10center_horizontal = true}}}


// 构建 ViewModel 实例 private val newsViewModel by lazy {// 构造 ViewModelProvider 实例, 通过其 get() 获得 ViewModel 实例 ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) }


override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(rootView)initView()bindData()}


// 将数据绑定到视图 private fun bindData() {newsViewModel.newsLiveData.observe(this, Observer {newsAdapter.news = itrvNews?.adapter = newsAdapter})}


private fun initView() {rvNews?.layoutManager = LinearLayoutManager(this)}}


其中构建布局 DSL 的详细介绍可以点击这里。它省去了原先V层( Activity + xml )中的 xml。


代码中的数据绑定是通过观察 ViewModel 中的 LiveData 实现的。这不是数据绑定的完全体,所以还需手动地观察 observe 数据变化(只有当引入data-binding包后,才能把视图和控件的绑定都静态化到 xml 中)。但至少它让 ViewModel 无需主动推数据了:


在 MVP 模式中,Presenter 持有 View 层接口并主动向界面推数据。


MVVM 模式中,ViewModel 不再持有 View 层接口,也不主动给界面推数据,而是界面被动地观察数据变化。


这使得 ViewModel 只需持有数据并根据业务逻辑更新之即可:


// 数据访问接口在构造函数中注入 class NewsViewModel(var newsRepository: NewsRepository) : ViewModel() {// 持有业务数据 val newsLiveData by lazy { newsRepository.fetchNewsLiveData() }}


// 定义构造 ViewModel 方法 class NewsFactory(context: Context) : ViewModelProvider.Factory {// 构造 数据访问接口实例 private val newsRepository = NewsRepositoryImpl(context)override fun <T : ViewModel?> create(modelClass: Class<T>): T {// 将数据接口访问实例注入 ViewModelreturn NewsViewModel(newsRepository) as T}}


// 然后就可以在 Activity 中这样构造 ViewModel 了

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
我是怎么把业务代码越写越复杂的 _ MVP - MVVM - Clean Architecture