前言
其实写MVVM?瞎搞一波?和MVVM?继续搞一波这两篇文章的时候没觉得水,但是后来自己看了一遍,感觉除了截了几张图之外并没说什么关于技术的东西,这就很扯淡了,技术文章没说技术。。。
接下来的一番话可能会得罪包括我在内的非常多的作者。。。
其实好多人写安卓是想当然的写,没错,就是想当然的写,也有可能只是我这样。
网上说的什么MVC、MVP、MVVM等等,说的都天花乱坠,也包括我之前发的两篇文章,说的挺好但是就没有例子,也没有思想,不对,有时候有思想,但是例子写的太简单根本没用,比如我之前看过好多作者写的MVP的文章,都是说一通MVP的意思是啥就占了很多的篇幅,再说下和MVC相比较有什么优势又写了不少,一大堆理论,实践的时候就写了一个登录页面,是,思想领悟到了,到底怎么写啊,哪家公司的安卓项目是光写一个登录页面,况且一个登录页面用MVP干啥,为了多写代码吗?增加代码的数量?照着写一个登录页面还好,整个项目怎么搭建?碰到特殊情况怎么处理?完完全全是摸石头过河!
所以准备写一个系列的文章,从最开始的项目搭建开始,一步一步地把写一个小项目的过程和思想尽力说明白,这就是文章诞生的原因。
项目呢就是之前写的MVVM版本的玩安卓,接口是泓洋大神现成的,大家可以下载apk先体验下:www.pgyer.com/llj2
再放下项目的Github地址:github.com/zhujiang521…
正文
也许好多人看见我上面写的那段话就不想看后面了。。。。那还写什么正文啊。。但,,应该还有人没骂我,那就能继续写了。。
如果让你重新从头到尾写一个项目,你第一步会干什么?
是不是把你问住了?有多少人一直在维护公司的一些项目,很久没这么干过了。来吧,今天开始在干一次吧!
MVC、MVP虽然不能说过时了,但毕竟是新项目,肯定要用最新的MVVM,又到了老生常谈的问题,什么是MVVM?前两篇文章其实解释的已经够多了,在这里就不赘述了,再说本篇文章的重点是怎么用!
第一步——写基类
我不知道大家写代码的习惯是什么,我个人习惯是新项目先搭建基类,然后再下手,因为不写好基类之后再抽取的话会很麻烦,那么安卓的基类是什么呢?肯定是 BaseActivity 和 BaseFragment。
说起 BaseActivity 和 BaseFragment,这里要写的东西一定要考虑好,因为这里的东西一定要是绝大多数类都能用到的方法,还有一些是要留给子类实现的。说到这里就需要想一下什么是绝大多数类都能用到的方法,看过项目介绍的应该知道项目实现了五种不同的状态:正常显示内容、加载中、没有网络、没有内容、加载错误,很显然,这些内容都应该写在 BaseActivity 和 BaseFragment 中,那么接下来就到了激动人心的码代码环节!
1、1 BaseActivity
abstract class BaseActivity : AppCompatActivity(){
/**
* Activity中显示加载等待的控件。
*/
private var loading: ProgressBar? = null
/**
* Activity中由于服务器异常导致加载失败显示的布局。
*/
private var loadErrorView: View? = null
/**
* Activity中由于网络异常导致加载失败显示的布局。
*/
private var badNetworkView: View? = null
/**
* Activity中当界面上没有任何内容时展示的布局。
*/
private var noContentView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViews()
}
protected open fun setupViews() {
loading = findViewById(R.id.loading)
noContentView = findViewById(R.id.noContentView)
badNetworkView = findViewById(R.id.badNetworkView)
loadErrorView = findViewById(R.id.loadErrorView)
if (loading == null) {
Log.e(TAG, "loading is null")
}
if (badNetworkView == null) {
Log.e(TAG, "badNetworkView is null")
}
if (loadErrorView == null) {
Log.e(TAG, "loadErrorView is null")
}
}
companion object {
private const val TAG = "BaseActivity"
}
}
好了,先放这么多,放太多会懵逼的。。。来看下代码吧:首先设置为抽象类是为啥就不说了,这个不知道的话该去学习 Java 基础了,然后把需要的 View 都找到,接下来就需要一个接口了,需要把在 Activity 或 Fragment 中进行数据请求所需要经历的生命周期函数抽出来,这样 BaseActivity 和 BaseFragment 就可以重复利用了,说干就干:
interface RequestLifecycle {
fun startLoading()
fun loadFinished()
fun loadFailed(msg: String?)
}
那么接下来接该改造下 BaseActivity 了:
abstract class BaseActivity : AppCompatActivity(), RequestLifecycle {
/**
* 当Activity中的加载内容服务器返回失败,通过此方法显示提示界面给用户。
*
* @param tip
* 界面中的提示信息
*/
protected fun showLoadErrorView(tip: String = "加载数据失败") {
loadFinished()
if (loadErrorView != null) {
val loadErrorText = loadErrorView?.findViewById<TextView>(R.id.loadErrorText)
loadErrorText?.text = tip
loadErrorView?.visibility = View.VISIBLE
return
}
}
/**
* 当Activity中的内容因为网络原因无法显示的时候,通过此方法显示提示界面给用户。
*
* @param listener
* 重新加载点击事件回调
*/
protected fun showBadNetworkView(listener: View.OnClickListener) {
loadFinished()
if (badNetworkView != null) {
badNetworkView?.visibility = View.VISIBLE
badNetworkView?.setOnClickListener(listener)
return
}
}
/**
* 当Activity中没有任何内容的时候,通过此方法显示提示界面给用户。
* @param tip
* 界面中的提示信息
*/
protected fun showNoContentView(tip: String) {
loadFinished()
val noContentText = noContentView?.findViewById<TextView>(R.id.noContentText)
noContentText?.text = tip
noContentView?.visibility = View.VISIBLE
}
/**
* 将load error view进行隐藏。
*/
private fun hideLoadErrorView() {
loadErrorView?.visibility = View.GONE
}
/**
* 将no content view进行隐藏。
*/
private fun hideNoContentView() {
noContentView?.visibility = View.GONE
}
/**
* 将bad network view进行隐藏。
*/
private fun hideBadNetworkView() {
badNetworkView?.visibility = View.GONE
}
@CallSuper
override fun startLoading() {
hideBadNetworkView()
hideNoContentView()
hideLoadErrorView()
loading?.visibility = View.VISIBLE
}
@CallSuper
override fun loadFinished() {
loading?.visibility = View.GONE
hideBadNetworkView()
hideNoContentView()
hideLoadErrorView()
}
@CallSuper
override fun loadFailed(msg: String?) {
loading?.visibility = View.GONE
hideBadNetworkView()
hideNoContentView()
hideLoadErrorView()
}
}
写到这里已经有大概的样子了,这里解释下 @CallSuper 这个注解:表示任何重写方法都应该调用此方法。接下来该干什么呢?刚才说过,有些很多类能用到,并且可以父类实现的咱们已经实现了,还有一种就是需要子类来实现的,比如:加载布局、加载页面、加载具体数据等等,不管是 Activity 或者是 Fragment 都需要,但是都必须是子类来实现的,那么也可以写一个接口来抽出来:
interface BaseInit {
fun initData()
fun initView()
fun getLayoutId(): Int
}
很清晰吧,加载数据、加载View、获取布局,那就可以继续完善下 BaseActivity 了:
abstract class BaseActivity : AppCompatActivity(), RequestLifecycle, BaseInit {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
transparentStatusBar()
setContentView(getLayoutId())
initView()
initData()
}
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setupViews()
}
/**
* 将状态栏设置成透明。只适配Android 5.0以上系统的手机。
*/
private fun transparentStatusBar() {
if (AndroidVersion.hasLollipop()) {
val decorView = window.decorView
decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.statusBarColor = Color.TRANSPARENT
}
}
}
这块来干了两件事:1、实现了初始化的接口并调用;2、将状态栏设置为透明,因为目前所有应用都实现了沉浸式。
不知道大家注意到没有,在父类中直接能实现的接口都实现了,但是需要在子类中实现的接口都没有实现,这就相当于在父类中直接写抽象方法,为了能和 BaseFragment 复用,所以提取成了接口,但并不在父类中进行实现从而交给了子类来实现。
最后再给 BaseActivity 加一个功能就差不多了:Activity 控制器,有很多情况下我们想把之前打开的 Activity 给一次性关闭,但是很麻烦,所以要实现一个 Activity 的控制器,每次 Activity 在执行 onCreate 方法的时候加入到控制器中,onDestroy 方法的时候从控制器中移除掉,来吧,写一个吧:
object ActivityCollector {
private const val TAG = "ActivityCollector"
private val activityList = ArrayList<WeakReference<Activity>?>()
fun size(): Int {
return activityList.size
}
fun add(weakRefActivity: WeakReference<Activity>?) {
activityList.add(weakRefActivity)
}
fun remove(weakRefActivity: WeakReference<Activity>?) {
val result = activityList.remove(weakRefActivity)
Log.d(TAG, "remove activity reference $result")
}
fun finishAll() {
if (activityList.isNotEmpty()) {
for (activityWeakReference in activityList) {
val activity = activityWeakReference?.get()
if (activity != null && !activity.isFinishing) {
activity.finish()
}
}
activityList.clear()
}
}
}
上面这个类很简单,只是一个 ArrayList ,进行添加和移除操作,这里需要注意的是为了防止内存泄露使用到了弱引用。
接下来就该把这个控制器添加到 BaseActivity 中了:
private var weakRefActivity: WeakReference<Activity>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityCollector.add(WeakReference(this))
weakRefActivity = WeakReference(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.remove(weakRefActivity)
}
好了,BaseActivity 到这里就差不多了,放一个完整版的吧:
abstract class BaseActivity : AppCompatActivity(), RequestLifecycle, BaseInit {
/**
* Activity中显示加载等待的控件。
*/
private var loading: ProgressBar? = null
/**
* Activity中由于服务器异常导致加载失败显示的布局。
*/
private var loadErrorView: View? = null
/**
* Activity中由于网络异常导致加载失败显示的布局。
*/
private var badNetworkView: View? = null
/**
* Activity中当界面上没有任何内容时展示的布局。
*/
private var noContentView: View? = null
private var weakRefActivity: WeakReference<Activity>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
transparentStatusBar()
setContentView(getLayoutId())
ActivityCollector.add(WeakReference(this))
weakRefActivity = WeakReference(this)
initView()
initData()
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.remove(weakRefActivity)
}
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setupViews()
}
protected open fun setupViews() {
loading = findViewById(R.id.loading)
noContentView = findViewById(R.id.noContentView)
badNetworkView = findViewById(R.id.badNetworkView)
loadErrorView = findViewById(R.id.loadErrorView)
if (loading == null) {
Log.e(TAG, "loading is null")
}
if (badNetworkView == null) {
Log.e(TAG, "badNetworkView is null")
}
if (loadErrorView == null) {
Log.e(TAG, "loadErrorView is null")
}
}
/**
* 将状态栏设置成透明。只适配Android 5.0以上系统的手机。
*/
private fun transparentStatusBar() {
if (AndroidVersion.hasLollipop()) {
val decorView = window.decorView
decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.statusBarColor = Color.TRANSPARENT
}
}
/**
* 当Activity中的加载内容服务器返回失败,通过此方法显示提示界面给用户。
*
* @param tip
* 界面中的提示信息
*/
protected fun showLoadErrorView(tip: String = "加载数据失败") {
loadFinished()
if (loadErrorView != null) {
val loadErrorText = loadErrorView?.findViewById<TextView>(R.id.loadErrorText)
loadErrorText?.text = tip
loadErrorView?.visibility = View.VISIBLE
return
}
}
/**
* 当Activity中的内容因为网络原因无法显示的时候,通过此方法显示提示界面给用户。
*
* @param listener
* 重新加载点击事件回调
*/
protected fun showBadNetworkView(listener: View.OnClickListener) {
loadFinished()
if (badNetworkView != null) {
badNetworkView?.visibility = View.VISIBLE
badNetworkView?.setOnClickListener(listener)
return
}
}
/**
* 当Activity中没有任何内容的时候,通过此方法显示提示界面给用户。
* @param tip
* 界面中的提示信息
*/
protected fun showNoContentView(tip: String) {
loadFinished()
val noContentText = noContentView?.findViewById<TextView>(R.id.noContentText)
noContentText?.text = tip
noContentView?.visibility = View.VISIBLE
}
/**
* 将load error view进行隐藏。
*/
private fun hideLoadErrorView() {
loadErrorView?.visibility = View.GONE
}
/**
* 将no content view进行隐藏。
*/
private fun hideNoContentView() {
noContentView?.visibility = View.GONE
}
/**
* 将bad network view进行隐藏。
*/
private fun hideBadNetworkView() {
badNetworkView?.visibility = View.GONE
}
@CallSuper
override fun startLoading() {
hideBadNetworkView()
hideNoContentView()
hideLoadErrorView()
loading?.visibility = View.VISIBLE
}
@CallSuper
override fun loadFinished() {
loading?.visibility = View.GONE
hideBadNetworkView()
hideNoContentView()
hideLoadErrorView()
}
@CallSuper
override fun loadFailed(msg: String?) {
loading?.visibility = View.GONE
hideBadNetworkView()
hideNoContentView()
hideLoadErrorView()
}
companion object {
private const val TAG = "BaseActivity"
}
}
BaseFragment
其实 BaseFragment 和 BaseActivity 基本一样,只是加载布局的地方有所不同,大家都是老司机,应该都懂:
/**
* Fragment中inflate出来的布局。
*/
private var rootView: View? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(getLayoutId(), container, false)
onCreateView(view)
return view
}
/**
* 在Fragment基类中获取通用的控件,会将传入的View实例原封不动返回。
* @param view
* Fragment中inflate出来的View实例。
* @return Fragment中inflate出来的View实例原封不动返回。
*/
private fun onCreateView(view: View): View {
rootView = view
loading = view.findViewById(R.id.loading)
noContentView = view.findViewById(R.id.noContentView)
badNetworkView = view.findViewById(R.id.badNetworkView)
loadErrorView = view.findViewById(R.id.loadErrorView)
if (loading == null) {
throw NullPointerException("loading is null")
}
if (badNetworkView == null) {
throw NullPointerException("badNetworkView is null")
}
if (loadErrorView == null) {
throw NullPointerException("loadErrorView is null")
}
return view
}
细心的大家应该都看出来了,在 BaseActivity 中如果 View 为空我只打印了 log 值,但在 BaseFragment 中却抛了异常!这里其实看需求来写,如果你认为你的实现都必须要实现 LCE ,那么就直接抛出,这样运行的时候就可以直接看出问题了,如果没必要的话打印个 log 值知道就可以了,没什么特别的深意。
LCE 布局
上面 BaseActivity 和 BaseFragment 中都提到的布局还没写呢!接下来写下布局吧:
一个一个来吧,先来没有内容的布局吧!
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wall">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dp_80"
android:layout_height="@dimen/dp_80"
android:layout_gravity="center_horizontal"
android:src="@drawable/no_content_image" />
<TextView
android:id="@+id/noContentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginBottom="@dimen/dp_20"
android:textSize="@dimen/sp_13"
android:textColor="@color/secondary_text"
tools:text="没有更多内容了"/>
</LinearLayout>
</RelativeLayout>
再来没有网络的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/badNetworkRootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wall"
android:focusable="true"
android:foreground="?android:selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dp_74"
android:layout_height="@dimen/dp_88"
android:layout_gravity="center_horizontal"
android:src="@drawable/bad_network_image" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginBottom="@dimen/dp_20"
android:text="@string/bad_network_view_tip"
android:textColor="@color/secondary_text"
android:textSize="@dimen/sp_13" />
</LinearLayout>
</RelativeLayout>
接下来是加载错误的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wall">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dp_74"
android:layout_height="@dimen/dp_88"
android:layout_gravity="center_horizontal"
android:src="@drawable/bad_network_image" />
<TextView
android:id="@+id/loadErrorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginBottom="@dimen/dp_20"
android:textColor="@color/secondary_text"
android:textSize="@dimen/sp_13"
tools:text="加载失败了" />
</LinearLayout>
</RelativeLayout>
还有加载中的布局:
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dp_64"
android:layout_gravity="center"
android:indeterminate="true" />
最后需要把这几个都合成到一个布局中:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/loading"
android:visibility="gone"/>
<include
android:id="@+id/noContentView"
layout="@layout/no_content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<include
android:id="@+id/badNetworkView"
layout="@layout/bad_network_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<include
android:id="@+id/loadErrorView"
layout="@layout/load_error_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>
第二步——使用BaseActivity
这一块本来想写下首页来着,但是想了想东西太多了,所以挑选了一个不需要联网的一个页面——浏览历史,这一个页面既继承了 BaseActivity,又有无内容、加载中、有内容等状态的切换,所以比较合适。
先来看一下页面的布局吧:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.profile.history.BrowseHistoryActivity">
<com.zj.core.util.TitleBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backImageVisiable="true"
app:titleName="浏览历史" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/historySmartRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/historyRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
<include layout="@layout/layout_lce" />
</FrameLayout>
</LinearLayout>
布局需要注意的是要把 layout_lce 写进去,layout_lce 就是咱们刚才编写的状态的布局,TitleBar 是我自定义的一个头布局,可设置标题、左边按钮、右边按钮,按钮的点击事件、图片或者问题都可以直接进行设置,大家可以进入 Github 中自行下载进行使用。
由于这个页面横竖屏无需做处理,所以只写一个页面即可。
布局写完了,下面就可以开始正式使用 BaseActivity 了:
class BrowseHistoryActivity : ArticleCollectBaseActivity() {
private lateinit var articleAdapter: ArticleAdapter
private var page = 1
override fun getLayoutId(): Int {
return R.layout.activity_browse_history
}
override fun initView() {
historyRecycleView.layoutManager = LinearLayoutManager(this)
articleAdapter = ArticleAdapter(
this,
R.layout.adapter_article,
// viewModel.articleList, //数据源
false
)
articleAdapter.setHasStableIds(true)
historyRecycleView.adapter = articleAdapter
historySmartRefreshLayout.apply {
setOnRefreshListener { reLayout ->
reLayout.finishRefresh(measureTimeMillis {
page = 1
// getArticleList() //加载数据
}.toInt())
}
setOnLoadMoreListener { reLayout ->
val time = measureTimeMillis {
page++
// getArticleList() //加载数据
}.toInt()
reLayout.finishLoadMore(if (time > 1000) time else 1000)
}
}
}
override fun initData() {
// getArticleList() //加载数据
}
companion object {
fun actionStart(context: Context) {
val intent = Intent(context, BrowseHistoryActivity::class.java)
context.startActivity(intent)
}
}
}
上面的代码就是使用BaseActivity,大家也可以看到,和正常使用 Activity 基本一致,只不过更加简洁了而已,最下面的伴生方法是给了其他类跳转到当前类的一个入口,这里看不出优势,但如果需要传其他参数的话效果就很好了,可以有效避免传错参数。
上面类还有一些内容没写完,剩下的是 MVVM 的内容,在下一个模块说。
第三步——使用MVVM
相信看过我之前两篇文章的老司机们已经会使用了,再来回顾一下吧!
VM 之前也说过,不是 ViewModel 但也是,不懂的可以去看下之前的文章。来看下 ViewModel 吧:
class BrowseHistoryViewModel(application: Application) : AndroidViewModel(application) {
private val pageLiveData = MutableLiveData<Int>()
val articleList = ArrayList<Article>()
val articleLiveData = Transformations.switchMap(pageLiveData) { page ->
BrowseHistoryRepository(application).getBrowseHistory(page)
}
fun getArticleList(page: Int) {
pageLiveData.value = page
}
}
是不是很简单,ViewModel + LiveData,就是这样,很简单是不是!
这里需要注意下使用到了 AndroidViewModel 。咱们平时使用的都是 ViewModel,有时候为了获取 Context 还需要单独传下参数,而 ViewModel 传参数又很麻烦,还需要使用 Factory 来传递,这种情况就可以使用 AndroidViewModel 了,可以直接继承进行使用,用的时候和之前一样就可以:
private val viewModel by lazy { ViewModelProvider(this).get(BrowseHistoryViewModel::class.java) }
是不是又 Get 到一个知识点,快记下来!
刚才的代码中在获取数据的地方都注释了,现在来看下吧!
private fun getArticleList() {
if (viewModel.articleList.size <= 0) {
startLoading()
}
viewModel.getArticleList(page)
}
override fun initData() {
viewModel.articleLiveData.observe(this, {
if (it.isSuccess) {
val articleList = it.getOrNull()
if (articleList != null) {
loadFinished()
if (page == 1 && viewModel.articleList.size > 0) {
viewModel.articleList.clear()
}
viewModel.articleList.addAll(articleList)
articleAdapter.notifyDataSetChanged()
} else {
showLoadErrorView()
}
} else {
if (viewModel.articleList.size <= 0) {
showNoContentView("当前无历史浏览记录")
} else {
showToast("没有更多数据")
loadFinished()
}
}
})
getArticleList()
}
这段代码信息量就比较大了,老司机们应该看到了刚才 BaseActivity 的方法:startLoading()、loadFinished()、showLoadErrorView()、showNoContentView("")等,其实原理很简单,根据数据的状态进行显示不同的页面即可。
再来看看 BrowseHistoryRepository 的代码吧:
class BrowseHistoryRepository(context: Context) {
private val browseHistoryDao = PlayDatabase.getDatabase(context).browseHistoryDao()
/**
* 获取历史记录列表
*/
fun getBrowseHistory(page: Int) = fire {
val projectClassifyLists = browseHistoryDao.getHistoryArticleList((page - 1) * 20,HISTORY)
if (projectClassifyLists.isNotEmpty()) {
Result.success(projectClassifyLists)
} else {
Result.failure(RuntimeException("response status is "))
}
}
}
到这里就很清晰了,Activity 用来展示页面,Repository 用来获取数据,ViewModel 用来处理数据和暂时保存数据以供 Activity 使用。
数据库肯定使用的是 Room ,这里要提一下,没用过 Room 的一定要使用下,如果你使用的是 Kotlin 的话更要使用了,Room 搭配上协程后简直不要太香!像上面的代码一样一行代码直接出结果,也无需进行线程的切换,因为这本来就是协程的擅长之处嘛!
总结
本来想写的东西很多,但是下笔却不知道如何进行描述,也怪自己,之前每个月能写几篇文章锻炼一下,现在一个月也就写一篇左右,是自己太懒了,忙不是借口,就是懒。。。
这一篇文章只是一个概览,是这个系列的第一篇文章,告诉大家一些看着神秘的东西到底是啥,该怎样使用,下一篇文章带大家看一看项目的首页是怎样一步一步搭建起来的。
MVVM 我感觉其实没有那么神秘,只是在工作中没有合适的项目用来练手,理解起来有些生疏,其实相对于 MVP 和 MVC 来说逻辑上更加清晰也更加方便了,毕竟有官方的 JetPack 来加持,肯定很香!文章虽然不长但也不短,还有好多细节没写到,大家如果有任何疑问,可以在评论区告诉我,或者是想让我写哪方面的内容也可以在评论区告诉我,以后我要坚持一个月最少两篇博客的更新!!!如果对大家有帮助的话别忘记三连,感谢🙏!