简评:继续第一部分的文章,做者在第二部分中使用的技术包括 MVVM,RxJava2.html
5. MVVM 架构 + 存储库模式 + Android 管理封装器
关于 Android 世界的一点点架构知识
长时间以来,Android 开发者们在他们的项目中没有使用任何类型的架构。近三年以来,架构在 Android 开发者社区中被炒得天花乱坠。Activity 之神的时代已通过去了,Google 发布了 Android 架构蓝图仓库,提供了许多样例和说明来实现不一样的架构方式。最终,在 Google IO 17 大会,他们介绍了 Android 架构组件,这些库的集合帮助咱们编写更清晰的代码和更好的 app。你可使用全部的组件,也可使用其中的部分。可是,我以为它们都挺有用的。接下来咱们将使用这些组件。我会先解决这些问题,而后用这些组件和库来重构代码,看看这些库解决了哪些问题。java
有两种主要的架构模式分离了 GUI 代码:android
很难说哪种更好。你应该两种都尝试一下,而后再作出决定。我倾向于使用管理生命周期组件的 MVVM 模式,而且接下来我会介绍它。若是你没试过 MVP,在 Medium 上有大量很好的关于它的文章。git
什么是 MVVM 模式?
MVVM 模式是一种架构模式。它表明 Model-View-ViewModel。我以为这个名字会让开发者困扰。若是我是那个命名的人,我会称之为 View-ViewModel-Model,由于 ViewModel 是链接视图和模型的中间件。数据库
MVVM,若是正确实现,将是一种很好的分离代码的方式,这样也会让它更容易测试。它帮助咱们遵循 SOLID 原则,所以咱们的代码更容易维护。api
如今我将写一个最简单的示例来解释它是怎样工做的。缓存
首先,建立一个简单的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 函数。如今,咱们的 refreshData 函数实现了 OnDataReadyCallback 。为了模拟等待,我使用了 Handler。2 秒后,OnDataReadyCallback 的实现将会调用 onDataReady 函数。架构
如今来建立咱们的ViewModel:
class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false }
能够看到,有一个 RepoModel 的示例,即将展现的 text 以及保存当前状态的 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,咱们应该把 view 改为加载中的状态,而且一旦获取到数据,就把 isLoading 设置为 false。同时,咱们应该把 text 改为ObservableField<String>,把 isLoading 改为 ObservableField<Boolean>。ObservableField 是一个数据绑定库中的类,咱们能够用它来建立一个可观察对象。它把对象包裹成可被观察的。
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.fleka.modernandroidapp.MainViewModel" /> </data>
而后:
... <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>
好,布局完成了。如今咱们来完成绑定。如咱们所说的 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() } }
最终,咱们的运行效果:
能够看到 old data 变成了 new data。
这就是最简单的 MVVM 示例。
还有一个问题,让咱们来旋转手机:
new data 又变成了 old data。怎么可能?看下 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 实例会不会好点?
隆重推出生命周期感知组件
由于许多的开发者面临这个问题,Android 框架团队的开发者决定建立一个库来帮咱们解决这个问题。ViewModel 类是其中一个。咱们全部的 ViewModel 类都应该继承自它。
让咱们的 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" }
而后继承 ViewModel:
package me.fleka.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... }
在 MainActivity 的 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 的实例了。如今,咱们从 ViewModelProviders 中获取它。ViewModelProviders 是一个功能类,有一个获取 ViewModelProvider 的方法。和做用范围相关,因此,若是你在 Activity 调用 ViewModelProviders.of(this) ,那么你的
ViewModel 会存活直到 Activity 被销毁(被销毁并且没有被从新建立)。相似地,若是你在 Fragment 中调用,你的 ViewModel 也会存活直到 Fragment 被销毁。看下下面的图解:
ViewModelProvider 的职责是在第一次调用的时候建立实例,并在 Activity/Fragment 从新建立时返回旧的实例。
不要混淆了:
MainViewModel::class.java
在 Kotlin 中,若是你仅仅写成:
MainViewModel::class
它会返回一个KClass,和 Java 中的 Class 不同。所以,若是咱们加上.java,它表示:
返回一个和给定的 KClass 实例关联的Java 类实例。
如今让咱们来旋转一下屏幕看看会发生什么:
咱们的数据和旋转以前同样。
上一篇文章中,我说过咱们的 app 将会获取 GitHub 仓库列表并展现。要想完成它,咱们须要添加
getRepositories 函数,它会返回一个伪造的仓库列表:
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>) }
同时,咱们的 MainViewModel 会有一个调用 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 我将使用 CardView 库,因此咱们要在 build.gradle 中添加依赖:
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.fleka.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>
下一步,在 activity_main.xml 中添加 RecyclerView。别忘了添加依赖:
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 函数。
如今,为 RecyclerView 添加一个适配器:
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 类型,这样咱们就能在 ViewHolder 中为每一项实现数据绑定。同时,别被下面一行函数给弄迷糊了:
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. } }
让咱们来运行试试:
很奇怪。发生了啥?
那么,MainViewModel 该怎样才能通知 MainActivity 更新了项目,好让咱们能够调用 notifyDataSetChanged 呢?
不该该这样作。
这点很是重要:MainViewModel 不该该知道任何关于MainActivity的东西。
MainActivity 才拥有 MainViewModel实例,因此应该让它来监听数据变化并通知Adapter。那怎么作?
咱们能够观察repositories,这样一旦数据改变了,咱们就能改变咱们的 adapter。
这个方案中可能出错的地方?
咱们先来看看下面的场景:
因此,咱们的方案还不够好。
介绍 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关键字是什么意思呢?若是某个函数只有一个参数,那么那个参数就能够用it来代替。假设咱们有个乘以 2 的 lambda 表达式:
((a) -> 2 * a)
咱们能够替换成这样:
(it * 2)
若是你如今运行,你会看到一切都正常工做了:
为何相比 MVP 我更倾向于 MVVM?
存储库模式
我以前说过,Model 是准备数据的抽象层。一般,它包括存储和数据类。每一个实体(数据)类都应该对应存储类。例如,若是咱们有个 User 和 Post 数据类,咱们应该也有 UserRepository 和 PostRepository 类。全部的数据都应该直接从它们中获取。咱们永远不该该在 View 或者 ViewModel 中调用 Shared Preferences 或者 DB 实例。
因此,咱们能够重命名咱们的 RepoModel 为 GitRepoRepository,GitRepo 从 GitHub 仓库中获取,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 或者 DB 实例直接去拿,但这仍然不是最佳实践。你必须尽量地模块化你的 app。若是你用不一样的客户端,用 Retrofit 替代 Volley 呢?若是你在里面写了一点逻辑,你很难去重构它。你的 repository 不须要知道你正在使用哪个客户端来获取远程数据。
我刚开始开发 Android 时,我曾经想知道应用时如何离线工做的,如何同步数据。好的应用架构容许咱们让这些变得简单。例如,当 ViewModel 中的 loadRepositories 被调用时,若是有链接网络,GitRepoRepositories 就能从远程数据源中获取数据,而后保存到本地。一旦手机处于离线模式,GitRepoRepository 就能从本地数据源获取数据。这样一来,Repositories 就应该有 RemoteDataSource 和 LocalDataSource 的实例,以及处理数据从哪里来的逻辑。
让咱们先来添加本地数据源:
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>) }
因此,分离数据源可让咱们更容易把数据保存到本地。
若是你只须要从网络获取数据,你仍须要存储库模式吗?是的。这会让你的代码更容易测试,其余开发者也能更好地理解你的代码,你也能够更快地维护。:)
Android 管理封装器
若是你想要在 GitRepoRepository 中检查网络链接,这样你就能够知道用哪一个数据源获取数据呢?咱们已经说过咱们不该该在 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" />
可是由于咱们没有 context,如何在 Repository 中建立实例呢?咱们能够在构造器中获得:
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 须要一个 Context,咱们怎样在 ViewModel 中拿到?你能够从生命周期感知的组件库中拿到 AndroidViewModel,它有一个 context。这个 context 是应用的上下文,而不是 Activity 的:
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 实例。因此在咱们的构造器中能够调用 super 方法,这样被咱们继承的 AndroidViewModel 的构造器就会被调用。
注意:咱们能够用一行代码来表示:
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 操做符会检查是否为空并返回一个 it 值。
接下来的文章中,我会介绍依赖注入,为何在 ViewModel 中建立 repository 实例是很差的,以及如何避免使用 AndroidViewModel。