使用Kotlin進行Android開發-第二部分

原文地址:https://proandroiddev.com/modern-android-development-with-kotlin-september-2017-part-2-17444fcdbe86

題外話
這兩篇文章覺得越看越有用,寫的實在是太好了,雖然只是簡單的帶你入門的知識,但是對于一直覺得Dagger,ButterKnife,MVVM這種不是特別快速入門的東西,真的是來一篇這么詳細的文章猶如醍醐灌頂,下面接著翻譯第二篇。

MVVM architecture + repository pattern + Android Manager Wrappers

請原諒我這個標題不知道怎么更清晰的描述,反正主要就是MVVM架構的東西。
關于Android世界中的機構方面的一些東西,長期以來,Android開發人員在項目中并沒有明確的架構的東西,在過去的三年中,Android架構如雨后春筍般的冒出來,谷歌發布了Android Architecture項目,其中包含了大量的架構方面的示例,在Google I/O 2017上,這一系列的庫,將使我們的項目具備整潔的代碼和更好的程序結構,你可以使用這些中的全部或者是一部分,在接下來的內容和這一系列的其他文章中,我們會使用這些庫,首先我會使用最原始的方法編碼,然后使用這些庫對代碼進行重構,以查看這些庫可以解決哪些問題。

下面有兩種分離代碼的主要架構:

  • MVP
  • MVVM

很難說哪一種更好,你需要嘗試過后選擇一個更適合你自己的,我給你個傾向于MVVM,使用lifecycle-aware庫,接下來我將會使用它,如果你也沒有嘗試過MVP,medium上面有很多關于MVP的有用的文章。

什么事MVVM模式

MVVM模式是一個架構模式,它代表Model-View-ViewModel,我認為這個名字會讓開發者混淆,如果我是一個為他命名的人,我會給他命名為View-ViewModel-Model,因為ViewModel是連接View和Model的橋梁。

View代表你的Activity,Fragment和其他Android中的自定義View的抽象,不要錯過這個View的重要性,View應該是愚蠢的,里面不應該有任何邏輯相關的東西,View不應該保存任何數據,他應該具有ViewModel實例的引用,并且需要從ViewModel來獲取所需要的數據,此外當ViewModel的數據被更改時,布局也應該被更改。

ViewModel是持有數據的類的抽象,并且具有關于核實應該獲取數據以及核實應該顯示數據的邏輯,ViewModel保存當前狀態,此外,ViewModel還持有了一個或多個Model的實例,并且從中獲取所有數據,例如數據是從本地數據庫獲取還是從遠程服務器,此外,ViewModel根本不用知道View,并且ViewModel也不用去了解Android的架構。

Model是我們為ViewModel準備數據的層,在這里我們將從遠程服務器獲取數據并將其緩存到內存中或保存到數據庫中。他和那些User,Car這些之持有數據的類不一樣,通常他是一個Repository模式的實現,接下來我們會介紹他。

如果你的MVVM架構寫的好的話,他會使我們的代碼更加易于測試和易于維護。

首先,讓我們創建一個簡單的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的接口,然后在refreshData方法中實現了他的方法,為了實現等待的效果,這里使用了Handler,兩秒過后,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的方法,每當你想實現一些接口或者擴展一些類而不想創建子類的話,你就可以使用對象申明,如果你想使用匿名內部類的方式,這種情況下,你不許使用對象表達式

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
        })
    }
}

當我們開始刷新數據的時候,我們界面上會變成加載的狀態,當數據獲取到之后,加載的狀態就會消失。
接著,我們需要把text變成ObservableField<String>,并且isLoading變成ObservableField<Boolean>,ObservableField是Data Binding庫中的一個類,用來創建一個可觀測的對象,它包裹了我們想要觀察的對象。

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)

接下來我們來改變我們的layout文件:

<data>
    <variable
        name="viewModel"
        type="me.fleka.modernandroidapp.MainViewModel" />
</data>

然后,

  • 更改TextView以觀察MainViewModel實例的text字段
  • 添加Progressbar,只在isLoading是true的時候顯示
  • 添加Button,用來刷新數據,并且只有在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}"
            />

當你運行的時候,你會發現有error,因為View.Visible和View.Gone在沒有導入View的時候是沒辦法直接使用的。

<data>
        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
</data>

現在我們來完成binding的部分,就像我上面說的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生命周期
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實例,怎么做我們才能在Activity重建的時候使用同一個ViewModel實例呢。

lifecycle-aware組件介紹

因為很多人都會面臨這個問題,ViewModel就是其中一個用來解決這個問題的,所以我們所有的ViewModel都需要繼承他。
讓我們改造一下我們的MainViewModel,讓他集成lifecycle-aware組件中的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.fleka.modernandroidapp

import android.arch.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    ...
}

然后,修改我們的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被銷毀(被銷毀而不是被重新創建)。



ViewModelProvider負責創建新的實例,如果它被首次調用,或者當你重新創建Activity/Fragment時會返回舊實例。
不要對下面的代碼疑惑:

MainViewModel::class.java

在Kotlin中,如果你使用下面的代碼

MainViewModel::class

它將返回一個KClass,它與Java中的Class不同。所以,如果我們這樣做:.java。
在文章的最后,我說過我們會從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中添加一個方法用來調用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布局
  • 添加RecyclerView到activity_main中
  • 編寫RepositoryRecyclerViewAdapter
  • 給RecyclerView設置Adapter

編寫rv_item_repository的時候,我們使用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,首先在build.gradle中添加:

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>

button按鈕出發getRepositories的方法用來替代refresh的方法

<Button
    android:id="@+id/refresh_button"
    android:onClick="@{() -> viewModel.loadRepositories()}" 
    ...
    />

刪除MainViewModel中的refresh方法,因為我們用不到了,接下來我們編寫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類型,因此我們可以對ViewHolder中的每個item實現數據綁定。不要為下面的一行代碼困惑:

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)
}

另外一個能讓你疑惑的點:

binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })

你可以替換參數為_,是不是很棒。
我們已經有了Adapter,但是我們還沒有設置給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創建了,adapter也創建了,但是repositories是空的。
  • 我們點擊按鈕
  • loadRepositories方法被調用了
  • 此時repositories是有數據的,但是我們沒有調用notifyDatasetChanged方法
  • 當我們旋轉屏幕的時候,activity重新創建了,adapter也重新創建了,但是repositories其實是已經有數據了。
    所以MainViewModel如何通知MainActivity有新數據了,所以我們需要調用notifyDatasetchanged方法。

這是不可能的。
重要的一點是MainViewModel不需要知道MainActivity的存在。

MainActivity是具有MainViewModel實例的,因此它應該是監聽更改并通知adapter有關更改的。
但是,如何做呢?
我么可以觀察repositories,一旦數據改變了,我們就可以改變我們的adapter。
讓我們看一下下面的情況:

  • 在MainActivity中,我們監聽repositories,一旦改變,我們去執行notifyDatasetChanged
  • 點擊按鈕
  • 當我們等待數據加載的時候,Activity會因為配置的改變從而導致重建。
  • 我們的MainViewModel還是存活的狀態.
  • 兩秒之后,我們獲取到了新的數據,然后通知觀察者數據改變了
  • 觀察者嘗試去調用不存在的adapter的notifyDatasetChanged方法,因為Activity重新創建了。

所以我們的解決方法并不好。

介紹LiveData

LiveData是lifecycle-aware組件中的另外一個類,他是基于觀察者的,可以知道View的生命周期,一旦Activity由于配置改變而銷毀的時候,LiveData是知道的,所以他也會銷毀觀察者
讓我們在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訪問,所以我們可以使用lambda表達式來替代:

((a) -> 2 * a) 

(it * 2)

我們再運行一下,就會發現一切正常了。

為什么相比MVP我更喜歡MVVM

  • 沒有令人厭煩的View的接口,因為ViewModel不需要引用View
  • 沒有令人厭煩的Presenter相關的接口,因為不需要
  • 更容易處理配置的更改
  • 使用MVVM,我們在Activity和Fragment中將實現更少的代碼

Repository模式

如前所述,Model只是我們準備數據的層的一個抽象名稱。通常它包含存儲庫和數據類。每個實體(數據)類應該有相應的Repository類。例如,如果我們有User和Post數據類,我們也應該有UserRepository和PostRepository。所有數據都應該直接來自它。我們不應該從View或ViewModel調用SharePreference實例或數據庫實例。
所以我們可以重命名我們的RepoModel為GitRepoRepository,GitRepo來自GitHub Repository,而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從GitHubRepository中獲取Github的Repository列表,但是GitRepoRepositories從哪來?
您可以直接在存儲庫中調用客戶端實例或數據庫實例,但這仍然不是一個好習慣。您的應用程序應盡可能多地進行模塊化。如果您決定使用不同的客戶端,用Retrofit替換Volley?如果你有一些邏輯,那就很難重構。您的存儲庫不需要知道您正在使用哪個客戶端來獲取遠程數據。

  • repository只需要知道的就是數據是從remote還是local過來的,不需要知道他如何獲取這些數據
  • ViewModel需要知道的就是數據
  • View只需要用來顯示數據

當我開始Android開發時,我想知道應用程序如何脫機工作以及數據同步的工作原理。應用程序的良好體系結構使我們能夠輕松實現這個功能。例如,當調用ViewModel中的loadRepositories時,如果有Internet連接,GitRepoRepositories可以從遠程數據源獲取數據并將其保存在本地數據源中。當手機處于脫機模式時,GitRepoRepository可以從本地數據源獲取數據。所以,Repository應該具有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 Manager Wrappers

如果你想檢查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
        }
}

上面的代碼只會在我們添加如下權限的時候工作:

<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在ViewModel中如何才能使用NetManager?您可以使用具有上下文的Lifecycle-aware組件庫中的AndroidViewModel。這個上下文是應用程序的上下文,而不是一個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 Note:操作符let檢查可空性,并在it中返回一個值.

終于翻譯完了,累死姐了,原作者太有良心了,每篇文章都是從原始的代碼到一步步優化,真的是寫的很棒,希望對看到的每一個人都有所幫助。

資源

https://developer.android.com/topic/libraries/architecture/lifecycle.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容