簡評:繼續第一部分的文章,做者在第二部分中使用的技術包括 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。