阅读 1829

玩安卓从 0 到 1 之总体概览

前言

其实写MVVM?瞎搞一波?MVVM?继续搞一波这两篇文章的时候没觉得水,但是后来自己看了一遍,感觉除了截了几张图之外并没说什么关于技术的东西,这就很扯淡了,技术文章没说技术。。。

接下来的一番话可能会得罪包括我在内的非常多的作者。。。

其实好多人写安卓是想当然的写,没错,就是想当然的写,也有可能只是我这样。

网上说的什么MVC、MVP、MVVM等等,说的都天花乱坠,也包括我之前发的两篇文章,说的挺好但是就没有例子,也没有思想,不对,有时候有思想,但是例子写的太简单根本没用,比如我之前看过好多作者写的MVP的文章,都是说一通MVP的意思是啥就占了很多的篇幅,再说下和MVC相比较有什么优势又写了不少,一大堆理论,实践的时候就写了一个登录页面,是,思想领悟到了,到底怎么写啊,哪家公司的安卓项目是光写一个登录页面,况且一个登录页面用MVP干啥,为了多写代码吗?增加代码的数量?照着写一个登录页面还好,整个项目怎么搭建?碰到特殊情况怎么处理?完完全全是摸石头过河!

所以准备写一个系列的文章,从最开始的项目搭建开始,一步一步地把写一个小项目的过程和思想尽力说明白,这就是文章诞生的原因。

项目呢就是之前写的MVVM版本的玩安卓,接口是泓洋大神现成的,大家可以下载apk先体验下:www.pgyer.com/llj2

img

再放下项目的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 来加持,肯定很香!文章虽然不长但也不短,还有好多细节没写到,大家如果有任何疑问,可以在评论区告诉我,或者是想让我写哪方面的内容也可以在评论区告诉我,以后我要坚持一个月最少两篇博客的更新!!!如果对大家有帮助的话别忘记三连,感谢🙏!