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

}

// 获取新闻
private fun fetchNews() {
// 1. 先从数据库读老新闻以快速展示
queryNews().let{ showNews(it) }
// 2. 再从网络拉新闻替换老新闻
newsApi.fetchNews(
mapOf(“page” to “1”,“count” to “4”)
).enqueue(object : Callback {
override fun onFailure(call: Call, t: Throwable) {
Toast.makeText(this@GodActivity, “network error”, Toast.LENGTH_SHORT).show()
}

override fun onResponse(call: Call, response: Response) {
response.body()?.result?.let {
// 3. 展示新新闻
showNews(it)
// 4. 将新闻入库
dbExecutor.submit { insertNews(it) }
}
}
})
}

// 从数据库读老新闻(伪代码)
private fun queryNews() : List {
val dbHelper = NewsDbHelper(this, …)
val db = dbHelper.getReadableDatabase()
val cursor = db.query(…)
var newsList = mutableListOf()
while(cursor.moveToNext()) {

newsList.add(news)
}
db.close()
return newsList
}

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

毕竟当时的关注点是实现功能,首要解决的问题是“如何绘制布局”、“如何操纵数据库”、“如何请求并解析网络数据”、“如何将数据填充在列表中”。待这些问题解决后,也没时间思考架构,所以就产生了上面的God Activity。Activity 管的太多了!Activity 知道太多细节:

  1. 异步细节
  2. 访问数据库细节
  3. 访问网络细节
  1. 如果大量 “细节” 在同一个层次被铺开,就显得啰嗦,增加理解成本。

拿说话打个比方:

你问 “晚饭吃了啥?”

“我用勺子一口一口地吃了鸡生下的蛋和番茄再加上油一起炒的菜。”

听了这样地回答,你还会和他做朋友吗?其实你并不关心他吃的工具、吃的速度、食材的来源,以及烹饪方式。

  1. 与 “细节” 相对的是 “抽象”,在编程中 “细节” 易变,而 “抽象” 相对稳定。

比如 “异步” 在 Android 中就有好几种实现方式:线程池、HandlerThread、协程、IntentService、RxJava。

  1. “细节” 增加耦合。

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 {
override fun onFailure(call: Call, t: Throwable) {
newsView.showNews(null)
}

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

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

newsList.add(news)
}
db.close()
return newsList
}

// 将新闻写入数据库(伪代码)
private fun insertNews(news : List) {
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?) {
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?)
// 获取界面上下文
abstract val newsContext:Context
}

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

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

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

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

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

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

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

这样还方便了自测,如果想测试各种临界数据产生时界面的表现,则可以实现一个PresenterForTest。如我是怎么把业务代码越写越复杂的 _ MVP - MVVM - Clean Architecture
果想覆盖业务逻辑的各种条件分支,则可以方便地给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 get(@NonNull String key, @NonNull Class 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);
}


}

ViewModelStoreViewModel实例存储在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
ity 销毁重建时被复用。

数据绑定

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

上一篇:disruptor无锁队列实现流水记录


下一篇:深入理解C语言的函数调用过程 【转】