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

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

前言

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

网上说的什么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 中,那么接下来就到了激动人心的码代码环节!

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 来加持,肯定很香!


目录
相关文章
|
6月前
|
安全 AndFix 网络安全
海外版交易所系统开发指南教程/需求步骤/案例详细/源码逻辑
Determine the functional requirements of the overseas version of the exchange system, including user registration, identity verification, trading, deposit and withdrawal, data statistics, etc
|
6月前
|
设计模式 前端开发 Java
KnowStreaming系列教程第二篇——项目整体架构分析
KnowStreaming系列教程第二篇——项目整体架构分析
84 0
|
Cloud Native Java 微服务
GitHub开源3小时,一直被哄抢!800页全彩《微服务架构深度解析》
阿嘴又来给大家分享好书了:王佩华老师的 《微服务架构深度解析:原理、实践与进阶》,也是网上还没开源出来的一本好书!阿嘴会在文末附电子版免费下载方式。
|
4月前
|
监控 安全 前端开发
交易所系统开发(源码正式版)/需求逻辑/玩法详情/规则架构
交易所源码开发是指基于特定的需求和要求,从头开始构建一个自定义的交易所平台的开发过程。这种开发可以包括以下几个关键方面:
|
6月前
|
监控 前端开发 关系型数据库
常见性能工具一览
今天写了一个调试工具的文章,就有人说起工具到底要会哪些。既然提到这儿了,那就多写几句吧。
151 2
常见性能工具一览
|
6月前
|
机器人
量化交易机器人系统开发详情源码/功能步骤/需求设计/稳定版
he development of a quantitative trading robot system involves multiple aspects, including strategy design, data processing, and transaction execution. The following is a detailed overview of the development strategy for a quantitative trading robot system:
|
6月前
|
Go
区域代理分红商城系统开发指南教程/步骤功能/方案逻辑/源码项目
The development of regional proxy dividend distribution mall system involves multiple aspects such as proxy dividend function and electronic mall system development. The following is an overview of the steps for developing a regional agent dividend distribution mall system
|
6月前
|
存储 分布式计算 数据处理
Alibaba最新神作!耗时182天肝出来1015页分布式全栈手册太香了
到底什么是分布式?这个话题一直以来就在各大平台论坛上被热议。一千个读者里面就有一千个哈姆雷特。官方这边给出的结论是:分布式就是将相同或相关的程序运行在多台计算机上,从而实现特定目标的一种计算方式。而从分布式技术的起源来看,随之诞生的分布式系统就是用更多的机器,处理更多的数据和更复杂的任务。
|
11月前
|
安全 算法 分布式数据库
ADA质押算力项目系统开发|详情方案|源码案例
智能合约作为Web3下的核心概念,具有巨大的潜力和应用前景