本文以一个真实项目的业务场景为载体,描述了经历一次次重构后,代码变得愈来愈复杂(you ya)的过程。java
本篇 Demo 的业务场景是:从服务器拉取新闻并在列表展现。android
刚接触 Android 时,我是这样写业务代码的(省略了和主题无关的 Adapter 和 Api 细节):git
class GodActivity : AppCompatActivity() {
private var rvNews: RecyclerView? = null private var newsAdapter = NewsAdapter() // 用 retrofit 拉取数据 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 dbExecutor = Executors.newSingleThreadExecutor() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.news_activity) initView() fetchNews() } private fun initView() { rvNews = findViewById(R.id.rvNews) rvNews?.layoutManager = LinearLayoutManager(this) } // 列表展现新闻 private fun showNews(news : List<News>) { newsAdapter.news = news rvNews?.adapter = newsAdapter } // 获取新闻 private fun fetchNews() { // 1. 先从数据库读老新闻以快速展现 queryNews().let{ showNews(it) } // 2. 再从网络拉新闻替换老新闻 newsApi.fetchNews( mapOf("page" to "1","count" to "4") ).enqueue(object : Callback<NewsBean> { override fun onFailure(call: Call<NewsBean>, t: Throwable) { Toast.makeText(this@GodActivity, "network error", Toast.LENGTH_SHORT).show() } override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) { response.body()?.result?.let { // 3. 展现新新闻 showNews(it) // 4. 将新闻入库 dbExecutor.submit { insertNews(it) } } } }) } // 从数据库读老新闻(伪代码) private fun queryNews() : List<News> { val dbHelper = NewsDbHelper(this, ...) 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(this, ...) val db = dbHelper.getWriteableDatabase() news.foreach { val cv = ContentValues().apply { ... } db.insert(cv) } db.close() } } 复制代码
毕竟当时的关注点是实现功能,首要解决的问题是“如何绘制布局”、“如何操纵数据库”、“如何请求并解析网络数据”、“如何将数据填充在列表中”。待这些问题解决后,也没时间思考架构,因此就产生了上面的God Activity
。Activity 管的太多了!Activity 知道太多细节:github
若是大量 “细节” 在同一个层次被铺开,就显得啰嗦,增长理解成本。
拿说话打个比方:web
你问 “晚饭吃了啥?”数据库
“我用勺子一口一口地吃了鸡生下的蛋和番茄再加上油一块儿炒的菜。”编程
听了这样地回答,你还会和他作朋友吗?其实你并不关心他吃的工具、吃的速度、食材的来源,以及烹饪方式。api
与 “细节” 相对的是 “抽象”,在编程中 “细节” 易变,而 “抽象” 相对稳定。
好比 “异步” 在 Android 中就有好几种实现方式:线程池、HandlerThread
、协程、IntentService
、RxJava
。缓存
“细节” 增长耦合。
GodActivity 引入了大量本和它无关的类:Retrofit
、Executors
、ContentValues
、Cursor
、SQLiteDatabase
、Response
、OkHttpClient
。Activity 本应该只和界面展现有关。服务器
既然 Activity 知道太多,那就让Presenter
来为它分担:
// 构造 Presenter 时传入 view 层接口 NewsView
class 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 层接口通知 Activity queryNews().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 层接口通知 Activity newsView.showNews(it) dbExecutor.submit { insertNews(it) } } } }) } // 从数据库读老新闻(伪代码) private fun queryNews() : List<News> { // 经过 view 层接口获取 context 构造 dbHelper val 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? = null private 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 = news rvNews?.adapter = newsAdapter } override val newsContext: Context get() = 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)持有数据。
上一节的例子中,构建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 实例的 Map private 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 获取 ViewModelStore this(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 implements LifecycleOwner, 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? = null private var newsAdapter = NewsAdapter() // 构建布局 private val rootView by lazy { ConstraintLayout { TextView { layout_id = "tvTitle" layout_width = wrap_content layout_height = wrap_content textSize = 25f padding_start = 20 padding_end = 20 center_horizontal = true text = "News" top_toTopOf = parent_id } rvNews = RecyclerView { layout_id = "rvNews" layout_width = match_parent layout_height = wrap_content top_toBottomOf = "tvTitle" margin_top = 10 center_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 = it rvNews?.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 { // 将数据接口访问实例注入 ViewModel return NewsViewModel(newsRepository) as T } } // 而后就能够在 Activity 中这样构造 ViewModel 了 class MvvmActivity : AppCompatActivity() { // 构建 ViewModel 实例 private val newsViewModel by lazy { ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) } } 复制代码
ViewModel
只关心业务逻辑和数据,不关心获取数据的细节,因此它们都被数据访问接口
隐藏了。
Demo 业务场景中,ViewModel
只有一行代码,那它还有存在的价值吗?
有!即便在业务逻辑如此简单的场景下仍是有!由于ViewModel
生命周期比 Activity 长,其持有的数据能够在 Activity 销毁重建时复用。
真实项目中的业务逻辑复杂度远高于 Demo,应该将业务逻辑的细节隐藏在ViewModel
中,让界面类无感知。好比 “将服务器返回的时间戳转化成年月日” 就应该写在ViewModel
中。
// 业务数据访问接口
interface NewsRepository { // 拉取新闻并以 LiveData 方式返回 fun fetchNewsLiveData():LiveData<List<News>?> } // 实现访问网络和数据库的细节 class NewsRepositoryImpl(context: Context) : NewsRepository { // 使用 Retrofit 构建请求访问网络 private val retrofit = Retrofit.Builder() .baseUrl("https://api.apiopen.top") .addConverterFactory(MoshiConverterFactory.create()) // 将返回数据组织成 LiveData .addCallAdapterFactory(LiveDataCallAdapterFactory()) .client(OkHttpClient.Builder().build()) .build() private val newsApi = retrofit.create(NewsApi::class.java) private var executor = Executors.newSingleThreadExecutor() // 使用 room 访问数据库 private var newsDatabase = NewsDatabase.getInstance(context) private var newsDao = newsDatabase.newsDao() private var newsLiveData = MediatorLiveData<List<News>>() override fun fetchNewsLiveData(): LiveData<List<News>?> { // 从数据库获取新闻 val localNews = newsDao.queryNews() // 从网络获取新闻 val remoteNews = newsApi.fetchNewsLiveData( mapOf("page" to "1", "count" to "4") ).let { Transformations.map(it) { response: ApiResponse<NewsBean>? -> when (response) { is ApiSuccessResponse -> { val news = response.body.result news?.let { // 将网络新闻入库 executor.submit { newsDao.insertAll(it) } } news } else -> null } } } // 将数据库和网络响应的 LiveData 合并 newsLiveData.addSource(localNews) { newsLiveData.value = it } newsLiveData.addSource(remoteNews) { newsLiveData.value = it } return newsLiveData } } 复制代码
这就是MVVM
中的M
,它定义了如何获取数据的细节。
Demo 中 数据库和网络都返回 LiveData 形式的数据,这样合并两个数据源只须要一个MediatorLiveData
。因此使用了 Room 来访问数据库。而且定义了LiveDataCallAdapterFactory
用于将 Retrofit 返回结果也转化成 LiveData。(其源码能够在这里找到)
这里也存在耦合:Repository
须要了解 Retrofit 和 Room 的使用细节。
当访问数据库和网络的细节愈来愈复杂,甚至又加入内存缓存时,再增长一层抽象,分别把访问内存、数据库、和网络的细节都隐藏起来,也是常见的作法。这样Repository
中的逻辑就变成: “运用什么策略将内存、数据库和网络的数据进行组合并返回给业务层”。
经屡次重构,代码结构不断衍化,最终引入了ViewModel
和Repository
。层次变多了,表面上看是愈来愈复杂了,但其实理解成本愈来愈低。由于 全部复杂的细节并非在同一层次被展开。
最后用 Clean architecture 再审视一下这套架构:
它是业务实体对象,对于 Demo 来讲 Entities 就是新闻实体类News
。
它是业务逻辑,Entities 是名词,Use Cases 就是用它造句。对于 Demo 来讲 Use Cases 就是 “展现新闻列表” 在 Clean Architecture 中每个业务逻辑都会被抽象成一个 UseCase 类,它被Presenters
持有,详情能够去这里了解
它是业务数据访问接口,抽象地描述获取和存储 Entities。和 Demo 中的 Repository 如出一辙,但在 Clean Architecture 中,它由 UseCase 持有。
它和MVP
模型中 Presenter 几乎同样,由它触发业务逻辑,并把数据传递给界面。惟一的不一样是,它持有 UseCase。
它是抽象业务数据访问接口的实现,和 Demo 中的NewsRepositoryImpl
如出一辙。
它是构建布局的细节,就像 Demo 中的 Activity。
它是和设备相关的细节,DB 和 UI 的实现细节也和设备有关,这里的 Device是指除了数据和界面以外的和设备相关的细节,好比如何在通知栏展现通知。
洋葱圈的内三层都是抽象,而只有最外层才包含实现细节(和 Android 平台相关的实现细节。好比访问数据库的细节、绘制界面的细节、通知栏提醒消息的细节、播放音频的细节)
洋葱圈向内的箭头意思是:外层知道相邻内层的存在,而内层不知道外层的存在。即外层依赖内层,内层不依赖外层。也就说应该尽量把业务逻辑抽象地实现,业务逻辑只须要关心作什么,而不应关心怎么作。这样的代码对扩展友好,当实现细节变化时,业务逻辑不须要变。
本文使用 mdnice 排版