我是怎么把业务代码越写越复杂的 _ MVP - MVVM - Clean Architecture
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 =
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);}
...}
ViewModelStore
将ViewModel
实例存储在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 了
评论