一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。
本文以一个真实项目的业务场景为载体,描述了经历一次次重构后,代码变得越来越复杂(you ya)的过程。
本篇 Demo 的业务场景是:从服务器拉取新闻并在列表展示。
GodActivity
刚接触 Android 时,我是这样写业务代码的(省略了和主题无关的 Adapter 和 Api 细节):
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 知道太多细节:
- 异步细节
- 访问数据库细节
- 访问网络细节
- 如果大量 “细节” 在同一个层次被铺开,就显得啰嗦,增加理解成本。
拿说话打个比方:
你问 “晚饭吃了啥?”
“我用勺子一口一口地吃了鸡生下的蛋和番茄再加上油一起炒的菜。”
听了这样地回答,你还会和他做朋友吗?其实你并不关心他吃的工具、吃的速度、食材的来源,以及烹饪方式。
- 与 “细节” 相对的是 “抽象”,在编程中 “细节” 易变,而 “抽象” 相对稳定。
比如 “异步” 在 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)持有数据。
生命周期更长的 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;
}
}
ViewModelStore
将ViewModel
实例存储在HashMap
中。
而ViewModelStore
通过ViewModelStoreOwner
获取:
// 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);
}
...
}
那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;
}
}
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
中的逻辑就变成: “运用什么策略将内存、数据库和网络的数据进行组合并返回给业务层”。
Clean Architecture
经多次重构,代码结构不断衍化,最终引入了ViewModel
和Repository
。层次变多了,表面上看是越来越复杂了,但其实理解成本越来越低。因为 所有复杂的细节并不是在同一层次被展开。
最后用 Clean architecture 再审视一下这套架构:

Entities
它是业务实体对象,对于 Demo 来说 Entities 就是新闻实体类News
。
Use Cases
它是业务逻辑,Entities 是名词,Use Cases 就是用它造句。对于 Demo 来说 Use Cases 就是 “展示新闻列表” 在 Clean Architecture 中每一个业务逻辑都会被抽象成一个 UseCase 类,它被Presenters
持有,详情可以去这里了解
Repository
它是业务数据访问接口,抽象地描述获取和存储 Entities。和 Demo 中的 Repository 一模一样,但在 Clean Architecture 中,它由 UseCase 持有。
Presenters
它和MVP
模型中 Presenter 几乎一样,由它触发业务逻辑,并把数据传递给界面。唯一的不同是,它持有 UseCase。
DB & API
它是抽象业务数据访问接口的实现,和 Demo 中的NewsRepositoryImpl
一模一样。
UI
它是构建布局的细节,就像 Demo 中的 Activity。
Device
它是和设备相关的细节,DB 和 UI 的实现细节也和设备有关,这里的 Device是指除了数据和界面之外的和设备相关的细节,比如如何在通知栏展示通知。
依赖方向
洋葱圈的内三层都是抽象,而只有最外层才包含实现细节(和 Android 平台相关的实现细节。比如访问数据库的细节、绘制界面的细节、通知栏提醒消息的细节、播放音频的细节)
洋葱圈向内的箭头意思是:外层知道相邻内层的存在,而内层不知道外层的存在。即外层依赖内层,内层不依赖外层。也就说应该尽可能把业务逻辑抽象地实现,业务逻辑只需要关心做什么,而不该关心怎么做。这样的代码对扩展友好,当实现细节变化时,业务逻辑不需要变。
网友评论