接着上一篇文章:应用 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 进阶路线、面试领导和简历模板送给你