前言
其实好多人写安卓是想当然的写,没错,就是想当然的写,也有可能只是我这样。
网上说的什么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 来加持,肯定很香!