ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-01-27 20:03:34  阅读:209  来源: 互联网

标签:MVP 实例 越写 MVVM ViewModel private Presenter val Activity


}

// 获取新闻
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。如
果想覆盖业务逻辑的各种条件分支,则可以方便地给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

标签:MVP,实例,越写,MVVM,ViewModel,private,Presenter,val,Activity
来源: https://blog.csdn.net/m0_65320833/article/details/122722835

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有