接着上一篇文章:应用Kotlin从零开始写一个古代Android 我的项目-Part1
5. MVVM架构+Repository模式+Android Manager
5.1 对于Android中的架构
长期以来,Android开发的我的项目中很少有架构,然而在过来几年,架构在各大Android社区宽泛宣传。Activity即所有的时代过来了,Google公布了一个仓库叫做 Android Architecture Blueprints
,它蕴含了许多示例和不同架构的阐明。最初,在Google IO/17大会上,公布了 Android Architecture Components
系列架构组件,能够帮忙咱们写更简洁、高质量的应用程序。你能够应用一个全副组件或者其中一个来构建你的应用程序,不过,我发现它们都十分有用,因而,本文剩下的局部和前面2局部中,我将介绍如何应用这些组件。首先,我将写一些有问题的代码,而后应用这些组件来重构,以看看这些库能帮咱们解决什么问题。
这里次要有两种架构模式
- MVP
- MVVM
很难说它两谁更好,你应该都试试当前再决定。我集体更喜爱带有生命周期组件的MVVM架构,本系列将围绕它来介绍,如果你还没有应用过MVP架构,Medium上有很多对于它的好文章,你能够去看看。
5.2 什么是MVVM模式?
MVVM模式是一种架构模式。它代表Model-View-ViewModel
。我认为这个名称会使开发人员感到困惑。如果我来命名它的话,我会将其命名为View-ViewModel-Model
,因为ViewModel是连贯View和Model的中间人。
其中View
是对你的Activity/Fragment/或者其余自定义View的形象名称,请留神,不要将它与Android 的View一概而论,这十分重要。View应该是洁净的,在View中,不应该蕴含任何逻辑代码,也不应该持有任何数据,他应该持有一个ViewModel
实例,所有的数据都应该从实例中去获取。此外,View应该察看这些数据,并且当ViewModel中的数据更改时,布局也应该刷新一次。总之,View的职责是:布局如何查找不同的数据和状态。
ViewModel
是保留数据的类的形象名称,并具备何时应获取数据以及应何时显示数据的逻辑。 ViewModel放弃以后状态。此外,ViewModel应该放弃一个或者多个Model实例,所有的数据都应该从这些Model实例获取。例如,ViewModel不应该晓得数据是来自数据库还是近程服务器。此外,ViewModel齐全不应该理解View。而且,ViewModel也齐全不应该理解Android框架层的货色。
Model
是数据层的形象名称。这是咱们将从近程服务器获取数据并将其缓存在内存中或保留在本地数据库中的类。然而请留神,这里的Model和Car
、User
、Square
这些model类是不一样,这些数据模型类仅仅只保持数据,而Model是Repository模式的实现,在后文将介绍,并且Model不应该理解ViewModel。
如果正确施行,MVVM是拆散代码并使其更具可测试性的好办法。它有助于咱们遵循SOLID准则,因而咱们的代码更易于保护。
代码示例
当初,我将写一个最简略的例子来阐明它是如何工作的
首先,让咱们创立一个简略的Model,该Model返回一些字符串:
class RepoModel {
fun refreshData() : String {
return "Some new data"
}
}
通常,获取数据是异步调用,因而咱们必须期待加载数据。为了模仿它,我将类更改为以下内容:
class RepoModel {
fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
}
}
interface OnDataReadyCallback {
fun onDataReady(data : String)
}
首先,咱们创立了一个接口OnDataReadyCallback
,它有一个办法onDataReady
,而后将OnDataReadyCallback
作为refreshData
的参数,用Handler来模仿期待,当2000ms后,调用接口实例的onDataReady
办法。
让咱们看一下ViewModel:
class MainViewModel {
var repoModel: RepoModel = RepoModel()
var text: String = ""
var isLoading: Boolean = false
}
如你所见,这里有一个RepoModel
实例,一个咱们要显示的 text
,和一个保留状态的boolean值isLoading
。当初,咱们创立一个refresh
办法,该办法负责获取数据
class MainViewModel {
...
val onDataReadyCallback = object : OnDataReadyCallback {
override fun onDataReady(data: String) {
isLoading.set(false)
text.set(data)
}
}
fun refresh(){
isLoading.set(true)
repoModel.refreshData(onDataReadyCallback)
}
}
refresh
办法调用了repoModel的refreshData
办法,传递了一个onDataReadyCallback
。然而等一会,object
是什么鬼?每当你要实现某个接口或扩大某些类而不创立子类时,都将应用对象申明。如果要应用它作为匿名类怎么办?在这种状况下,您必须应用对象表达式:
class MainViewModel {
var repoModel: RepoModel = RepoModel()
var text: String = ""
var isLoading: Boolean = false
fun refresh() {
repoModel.refreshData( object : OnDataReadyCallback {
override fun onDataReady(data: String) {
text = data
})
}
}
当咱们调用refresh时,咱们应该将视图更改为加载状态
,一旦数据到来,就应该将isLoading
设置为false
。
另外,咱们应该将text
更改为ObservableField <String>
,并将isLoading
更改为ObservableField <Boolean>
。 ObservableField是Data Binding库中的一个类,咱们能够应用它代替创立Observable对象。它包装了咱们想要察看的对象。
class MainViewModel {
var repoModel: RepoModel = RepoModel()
val text = ObservableField<String>()
val isLoading = ObservableField<Boolean>()
fun refresh(){
isLoading.set(true)
repoModel.refreshData(object : OnDataReadyCallback {
override fun onDataReady(data: String) {
isLoading.set(false)
text.set(data)
}
})
}
}
留神,我应用val
而不是var
,因为咱们仅更改字段中的值,而不更改字段自身,如果要初始化它,则应该执行以下操作:
val text = ObservableField("old data")
val isLoading = ObservableField(false)
咱们更改布局,以让它能够察看text
和isLoading
,首先,咱们将绑定MainViewModel而不是Repository:
<data>
<variable
name="viewModel"
type="me.mladenrakonjac.modernandroidapp.MainViewModel" />
</data>
而后,做一下操作:
- 更改TextView以察看MainViewModel实例上的
text
- 增加仅在
isLoading
为true
时可见的ProgressBar - 单击的add按钮将从MainViewModel实例调用refresh函数,并且仅在
isLoading
为false
时才可单击
...
<TextView
android:id="@+id/repository_name"
android:text="@{viewModel.text}"
...
/>
...
<ProgressBar
android:id="@+id/loading"
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
...
/>
<Button
android:id="@+id/refresh_button"
android:onClick="@{() -> viewModel.refresh()}"
android:clickable="@{viewModel.isLoading ? false : true}"
/>
...
如果此时你运行程序,将会报错,起因是,如果未导入View,则无奈应用View.VISIBLE
和View.GONE
。因而,咱们必须导入它:
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
ok,布局就到此实现,接下来该实现绑定了,如咱们所说,View应该持有一个ViewModel实例:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var mainViewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
binding.executePendingBindings()
}
}
最初,咱们能够运行它了。
您能够看到旧数据
已更改为新数据
。
这是最简略的MVVM示例。
对此有一个问题,让咱们当初旋转手机:
新数据
又变回了旧数据
。这怎么可能呢?看一下Activity的生命周期:
旋转屏幕后,将创立Activity的新实例,并调用onCreate()
办法。当初,看看咱们的Activity:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var mainViewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
binding.executePendingBindings()
}
}
如您所见,一旦创立了一个新的Activity实例,便也会创立一个新的MainViewModel实例。如果以某种形式,咱们能够为每个从新创立的MainActivity具备雷同的MainViewModel实例,那会很好吗?
Lifecycle-aware 组件介绍
因为许多开发人员都遇到了这个问题,因而Android Framework Team的开发人员决定开发可帮忙咱们解决这个问题的库。 ViewModel类就是其中之一。它是咱们所有ViewModels都应该扩大的类。
让咱们的MainViewModel 继承自有生命周期感知的组件ViewModel,首先,咱们应该在build.gradle
文件中增加该生命周期感知组件库(译者注:版本不是最新,应用时更新最新版本):
dependencies {
...
implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
}
MainViewModel继承自ViewModel,如下:
package me.mladenrakonjac.modernandroidapp
import android.arch.lifecycle.ViewModel
class MainViewModel : ViewModel() {
...
}
在Activity的onCreate办法中,你应该改为:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.executePendingBindings()
}
}
请留神,咱们并没有创立一个新的MainViewModel
实例,咱们从ViewModelProvider中获取它,ViewModelProviders是一个工具类,它有获取ViewModel实例的办法。与范畴相干,如果你在Activity中调用ViewModelProviders.of(this)
,则ViewModel会始终存在,晓得Activity被彻底销毁(销毁没有被重建),同样的,如果你在Fragment中调用,ViewModel会始终存在,直到Fragment被彻底销毁。看看下图:
ViewModelProvider负责在第一次调用时创立新实例,或在从新创立Activity / Fragment时返回旧实例。
请勿与以下内容混同:
MainViewModel::class.java
在Kotlin中,如果你向上面这样写:
MainViewModel::class
它将返回一个KClass
,它与Java中的Class不同。因而,咱们须要加一个.java
后缀。
返回与给定KClass实例绝对应的Java Class实例。
让咱们看一下,旋转屏幕会产生什么?
咱们领有与之前旋转时雷同的数据。
在上一篇文章中,我说过咱们的应用程序将获取Github仓库列表并显示它。为此,咱们必须增加getRepositories
函数,该函数将返回mock的仓库列表:
class RepoModel {
fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
}
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First", "Owner 1", 100 , false))
arrayList.add(Repository("Second", "Owner 2", 30 , true))
arrayList.add(Repository("Third", "Owner 3", 430 , false))
Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000)
}
}
interface OnDataReadyCallback {
fun onDataReady(data : String)
}
interface OnRepositoryReadyCallback {
fun onDataReady(data : ArrayList<Repository>)
}
与之对应,在ViewModel中,也应该有一个函数,该函数调用RepoModel中的getRepositories
函数。
class MainViewModel : ViewModel() {
...
var repositories = ArrayList<Repository>()
fun refresh(){
...
}
fun loadRepositories(){
isLoading.set(true)
repoModel.getRepositories(object : OnRepositoryReadyCallback{
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories = data
}
})
}
}
最初,咱们应该在RecyclerView中显示这仓库列表。为此,咱们将必须:
- 增加一个
rv_item_repository.xml
布局 - 在
activity_main.xml
增加RecyclerView - 增加
RepositoryRecyclerViewAdapter
适配器 - 给RecycklerView设置Adapter
为了让rv_item_repository.xml
应用CardView,须要在build.gradle
中增加(译者注:最新的请应用androidx):
implementation 'com.android.support:cardview-v7:26.0.1'
而后,布局像上面这样:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<import type="android.view.View" />
<variable
name="repository"
type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_margin="8dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/repository_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@{repository.repositoryName}"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.083"
tools:text="Modern Android App" />
<TextView
android:id="@+id/repository_has_issues"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/has_issues"
android:textStyle="bold"
android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="@+id/repository_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/repository_name"
app:layout_constraintTop_toTopOf="@+id/repository_name"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="@+id/repository_owner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@{repository.repositoryOwner}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repository_name"
app:layout_constraintVertical_bias="0.0"
tools:text="Mladen Rakonjac" />
<TextView
android:id="@+id/number_of_starts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{String.valueOf(repository.numberOfStars)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repository_owner"
app:layout_constraintVertical_bias="0.0"
tools:text="0 stars" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</layout>
下一步是将RecyclerView增加到main_activity.xml
,在这之前,别忘了增加:
implementation 'com.android.support:recyclerview-v7:26.0.1'
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.fleka.modernandroidapp.MainActivity">
<ProgressBar
android:id="@+id/loading"
android:layout_width="48dp"
android:layout_height="48dp"
android:indeterminate="true"
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@+id/refresh_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/repository_rv"
android:layout_width="0dp"
android:layout_height="0dp"
android:indeterminate="true"
android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toTopOf="@+id/refresh_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/rv_item_repository" />
<Button
android:id="@+id/refresh_button"
android:layout_width="160dp"
android:layout_height="40dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:onClick="@{() -> viewModel.loadRepositories()}"
android:clickable="@{viewModel.isLoading ? false : true}"
android:text="Refresh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
</android.support.constraint.ConstraintLayout>
</layout>
请留神,咱们从之前布局删除了一些TextView元素,并且按钮当初触发loadRepositories
函数,而不是refresh:
<Button
android:id="@+id/refresh_button"
android:onClick="@{() -> viewModel.loadRepositories()}"
...
/>
而后,咱们删除MainViewModel中的refresh
函数和RepoModel中的refreshData
函数,因为咱们不再须要它们了。
当初,咱们增加一个Adapter
class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>,
private var listener: OnItemClickListener)
: RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int)
= holder.bind(items[position], listener)
override fun getItemCount(): Int = items.size
interface OnItemClickListener {
fun onItemClick(position: Int)
}
class ViewHolder(private var binding: RvItemRepositoryBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(repo: Repository, listener: OnItemClickListener?) {
binding.repository = repo
if (listener != null) {
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
}
binding.executePendingBindings()
}
}
}
请留神,ViewHolder 持有的是RvItemRepositoryBinding
类型的实例而不是View类型,这样咱们就能够在Item中应用Data Binding了,另外,不要被上面这行代码搞蛊惑了:
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener)
它只是上面这个函数的简写:
override fun onBindViewHolder(holder: ViewHolder, position: Int){
return holder.bind(items[position], listener)
}
而items [position]
是索引运算符的实现。它与items.get(position)
雷同。
另一行代码也可能让你困惑:
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
你能够将参数用_
替换,很酷,是吧?
咱们增加了适配器,但仍未在MainActivity中将其设置给recyclerView:
class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
binding.executePendingBindings()
binding.repositoryRv.layoutManager = LinearLayoutManager(this)
binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this)
}
override fun onItemClick(position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
很奇怪,这里产生了什么?
- Activity被创立,因而应用理论为空的
repositories
创立了新适配器 - 咱们点击了按钮
- 调用
loadRepositories
函数,显示进度 - 2秒后,咱们失去了repositories,进度被暗藏了,然而列表却没有显示。这是因为未在适配器上调用
notifyDataSetChanged
- 旋转屏幕后,将创立新的Activity,因而将应用带有一些内容的
repositories
参数创立新的适配器
因而,MainViewModel应该如何告诉MainActivity无关新Item的信息,以便咱们能够调用notifyDataSetChanged
?
这不应该是ViewModel来做的
这十分重要,MainViewModel应该理解MainActivity
,MainActivity是领有MainViewModel实例的,因而它是应该侦听更改并告诉Adapter无关更改。
那?到底该如何做呢?
咱们要能够察看repositories
,当他扭转的时候,告诉列表更新。
该解决方案有什么问题没?
咱们看一下以下场景:
- 在MainActivity中察看
repositories
,一旦它放生更改,咱们调用notifyDataSetChanged
- 咱们点击按钮
- 在咱们期待数据更改时,因为配置更改,能够从新创立MainActivity。
- 但咱们的MainViewModel依然存在。
- 2秒后,“repositories”字段获取新我的项目,并告诉观察者数据已更改
- 观察者尝试对不再存在的适配器执
行notifyDataSetChanged
,因为从新创立了MainActivity。
而后程序就崩了,因而下面的计划不够好。咱们得引入一个新的组件你LiveData
LiveData介绍
LiveData是另一个生命周期感知组件,它能够察看View的生命周期,因而,一旦Activity因为配置更改而被销毁时,LiveData就会感知到它,而后它就会从被销毁的Activity中勾销观察者的订阅。
让咱们在MainViewModel中实现它:
class MainViewModel : ViewModel() {
var repoModel: RepoModel = RepoModel()
val text = ObservableField("old data")
val isLoading = ObservableField(false)
var repositories = MutableLiveData<ArrayList<Repository>>()
fun loadRepositories() {
isLoading.set(true)
repoModel.getRepositories(object : OnRepositoryReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories.value = data
}
})
}
}
并察看MainActivity的变动:
class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
private lateinit var binding: ActivityMainBinding
private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
binding.executePendingBindings()
binding.repositoryRv.layoutManager = LinearLayoutManager(this)
binding.repositoryRv.adapter = repositoryRecyclerViewAdapter
viewModel.repositories.observe(this,
Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} })
}
override fun onItemClick(position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
下面的it关键字代表什么呢?在Kotlin中,如果,函数只有一个参数,那么它默认你会被替换成it
,假如咱们有一个乘以2的lamuda表达式:
((a) -> 2 * a)
能够写成上面这样 :
(it * 2)
当初你运行程序,所有都失常工作了!
我为什么喜爱MVVM而不是MVP
- 没有那些提供给View的无聊接口,因为ViewModel没有对View的援用
- 也没有提供给Presenter的接口,因为不须要
- 它解决配置更改是如此简略
- 应用MVVM,咱们Activity/Fragment中的代码更简洁
Repository 模式
正如我后面所说,Model只是数据层的一个形象,通常来说,它蕴含repositories
和数据类,每一个实体(data)类应该有一个对应的Repository
类。例如,咱们有一个User和一个Post数据类,那么也应该对应有UserRepository和PostRepository,所有的数据都应该从Repository处获取。咱们不应该在View或者ViewModel中间接应用Shared Preferences或者DB。
因而,咱们能够将RepoModel重命名为GitRepoRepository,其中GitRepo来自Github存储库,而Repository来自Repository模式。
class GitRepoRepository {
fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First", "Owner 1", 100, false))
arrayList.add(Repository("Second", "Owner 2", 30, true))
arrayList.add(Repository("Third", "Owner 3", 430, false))
Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000)
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
好的,MainViewModel从GitRepoRepsitories获取Github 仓库列表,然而GitRepoRepositories
数据从何而来?
你能够间接在Repository中对客户端实例或数据库实例进行调用,但这依然不是一个好习惯。你的应用程序应尽可能模块化。如果你决定应用其余Client,用Retrofit代替Volley怎么办?如果你有一些逻辑在外面,将很难对其进行重构。你的存储库不须要晓得你要应用哪个客户端来获取近程数据。
- repository 仅须要晓得数据是来自近程还是本地,而不须要晓得是如何从近程或者本地获取
- ViewModel仅只须要数据
- View 仅须要显示数据
当我刚开始开发Android的时候,我就在想,APP如何在离线状况下工作?数据同步是怎么实现的?好的架构是咱们很容易做到这些。比方,当loadRepositories
在ViewModel中被调用的时候,如果网络链接失常,GitRepoRepositories从近程获取数据,并且将数据保留到本地数据源,一旦手机处于离线模式,GitRepoRepositories能够从本地数据源获取数据,因而,Repositories须要有持有近程数据源实例RemoteDataSource
和本地数据源实例LocalDataSource
和解决数据从哪里来的逻辑。
让咱们增加一个本地数据源(local data source):
class GitRepoLocalDataSource {
fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First From Local", "Owner 1", 100, false))
arrayList.add(Repository("Second From Local", "Owner 2", 30, true))
arrayList.add(Repository("Third From Local", "Owner 3", 430, false))
Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000)
}
fun saveRepositories(arrayList: ArrayList<Repository>){
//todo save repositories in DB
}
}
interface OnRepoLocalReadyCallback {
fun onLocalDataReady(data: ArrayList<Repository>)
}
在这里,咱们有两个办法:第一个返回伪造的本地数据,第二个用于保留伪造数据。
让咱们增加近程数据源:
class GitRepoRemoteDataSource {
fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First from remote", "Owner 1", 100, false))
arrayList.add(Repository("Second from remote", "Owner 2", 30, true))
arrayList.add(Repository("Third from remote", "Owner 3", 430, false))
Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000)
}
}
interface OnRepoRemoteReadyCallback {
fun onRemoteDataReady(data: ArrayList<Repository>)
}
它仅有一个办法返回近程模仿数据
接下来为repository增加一些逻辑:
class GitRepoRepository {
val localDataSource = GitRepoLocalDataSource()
val remoteDataSource = GitRepoRemoteDataSource()
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
localDataSource.saveRepositories(data)
onRepositoryReadyCallback.onDataReady(data)
}
})
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
因而,拆散数据源,咱们能够轻松地在本地保留数据。
如果你只须要来自网络的数据该怎么办》?还须要应用Repository模式吗?是。它使您的代码更易于测试,其余开发人员能够更好地了解您的代码,并且能够更快地对其进行保护! 🙂
Android Manager 包装器
如果要在GitRepoRepository中查看Internet连贯,以便能够晓得要查问哪个数据源,该怎么办?咱们曾经说过,咱们不应该在ViewModels和Models中搁置任何与Android相干的代码,那么如何解决这个问题呢?
咱们为网络连接写一个包装器
class NetManager(private var applicationContext: Context) {
private var status: Boolean? = false
val isConnectedToInternet: Boolean?
get() {
val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val ni = conManager.activeNetworkInfo
return ni != null && ni.isConnected
}
}
下面的代码须要咱们在Manifest中增加权限之后能力工作
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
然而,怎么在Repository创立实例呢?因为咱们没有context啊,当然,能够从构造方法传入
class GitRepoRepository (context: Context){
val localDataSource = GitRepoLocalDataSource()
val remoteDataSource = GitRepoRemoteDataSource()
val netManager = NetManager(context)
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
localDataSource.saveRepositories(data)
onRepositoryReadyCallback.onDataReady(data)
}
})
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
在咱们为ViewModel创立一个新的GitRepoRepository实例之前呢,咱们如何失去一个NetManager呢?因为咱们须要给NetManager传一个Context,你能够应用生命周期感知组件中的AndroidViewModel,它带有Contenxt,它的Context是一个Application Context而不是一个Activity的Context。
class MainViewModel : AndroidViewModel {
constructor(application: Application) : super(application)
var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication()))
val text = ObservableField("old data")
val isLoading = ObservableField(false)
var repositories = MutableLiveData<ArrayList<Repository>>()
fun loadRepositories() {
isLoading.set(true)
gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback {
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories.value = data
}
})
}
}
这行代码:
constructor(application: Application) : super(application)
咱们正在为MainViewModel定义构造函数。这是必须的,因为AndroidViewModel在其构造函数中要求一个Application实例。因而,在咱们的构造函数中,须要调用AndroidViewModel的构造函数的super办法,以便能够调用咱们继承类的构造函数。
留神:咱们能够简写成一行:
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
}
当初,在咱们的GitRepoRepository中有一个NetManager实例了,能够查看网络连接状态。
class GitRepoRepository(val netManager: NetManager) {
val localDataSource = GitRepoLocalDataSource()
val remoteDataSource = GitRepoRemoteDataSource()
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
netManager.isConnectedToInternet?.let {
if (it) {
remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
override fun onRemoteDataReady(data: ArrayList<Repository>) {
localDataSource.saveRepositories(data)
onRepositoryReadyCallback.onDataReady(data)
}
})
} else {
localDataSource.getRepositories(object : OnRepoLocalReadyCallback {
override fun onLocalDataReady(data: ArrayList<Repository>) {
onRepositoryReadyCallback.onDataReady(data)
}
})
}
}
}
}
interface OnRepositoryReadyCallback {
fun onDataReady(data: ArrayList<Repository>)
}
因而,如果咱们有网络连接,咱们将获取近程数据并将其保留在本地。另一方面,如果没有网络连接,咱们将获取本地数据。
Kotlin提醒: let运算符能够查看可控性,并且从其中返回一个值
预报
在前面的文章中,我将介绍依赖项注入,为什么在ViewModel中创立存储库实例很蹩脚,以及如何防止应用AndroidViewModel。另外,到目前为止,写的代码中有一些问题,这样写是有起因的,我试图让你面对这些问题,以便你能够了解为什么所有这些库都很受欢迎以及为什么要应用它。
最初,感激你的浏览!
本系列已更新结束:
【译】应用Kotlin从零开始写一个古代Android 我的项目-Part1
【译】应用Kotlin从零开始写一个古代Android 我的项目-Part2
【译】应用Kotlin从零开始写一个古代Android 我的项目-Part3
【译】应用Kotlin从零开始写一个古代Android 我的项目-Part4
文章首发于公众号:
「 技术最TOP 」
,每天都有干货文章继续更新,能够微信搜寻「 技术最TOP 」
第一工夫浏览,回复【思维导图】【面试】【简历】有我筹备一些Android进阶路线、面试领导和简历模板送给你
发表回复