用 Kotlin 開發現代 Android 項目 Part 2

簡評:繼續第一部分的文章,作者在第二部分中使用的技術包括 MVVM,RxJava2.

5. MVVM 架構 + 存儲庫模式 + Android 管理封裝器

關于 Android 世界的一點點架構知識

長時間以來,Android 開發者們在他們的項目中沒有使用任何類型的架構。近三年以來,架構在 Android 開發者社區中被炒得天花亂墜。Activity 之神的時代已經過去了,Google 發布了 Android 架構藍圖倉庫,提供了許多樣例和說明來實現不同的架構方式。最終,在 Google IO 17 大會,他們介紹了 Android 架構組件,這些庫的集合幫助我們編寫更清晰的代碼和更好的 app。你可以使用所有的組件,也可以使用其中的部分。但是,我覺得它們都挺有用的。接下來我們將使用這些組件。我會先解決這些問題,然后用這些組件和庫來重構代碼,看看這些庫解決了哪些問題。

有兩種主要的架構模式分離了 GUI 代碼:

  • MVP
  • MVVM

很難說哪一種更好。你應該兩種都嘗試一下,然后再做出決定。我傾向于使用管理生命周期組件的 MVVM 模式,并且接下來我會介紹它。如果你沒試過 MVP,在 Medium 上有大量很好的關于它的文章。

什么是 MVVM 模式?

MVVM 模式是一種架構模式。它代表 Model-View-ViewModel。我覺得這個名字會讓開發者困擾。如果我是那個命名的人,我會稱之為 View-ViewModel-Model,因為 ViewModel 是連接視圖和模型的中間件。

  • View 是你的 Activity,Fragment 或者其他 Android 自定義視圖的抽象。注意千萬別和 Android View 混淆了。這個 View 應該是個啞巴,我們不能在 View 里寫任何邏輯。視圖不應該持有任何數據。它應該有一個 ViewModel 實例,所有 View 需要的數據應該從這個實例中獲取。同時,View 應該觀察這些數據,一旦 ViewModel 中的數據發生了變化,布局就會發生改變。View 有一個職責:不同的數據和狀態下,布局會怎樣顯示。

  • ViewModel 是持有數據和邏輯類的一個抽象,負責什么時候獲取數據,什么時候傳遞數據。ViewModel 保留當前的狀態。同時,ViewModel 持有一個或多個 Model 實例,所有的數據都從這些實例中獲取。它不應該知道這些數據是從數據庫中獲取的還是從遠程服務器中獲取的。此外,ViewModel 也不應該知道關于 View 的一切。而且,ViewModel 也不應該知道任何關于 Android 框架的事。

  • Model 是為 ViewModel 準備數據的抽象層。它是那些從遠程服務器或者內存緩存或者本地數據庫獲取數據的類。這和那些 User,Car,Square 等等是不一樣的。模型類知識一些保存數據的類。通常,它是倉庫模式的一種實現,我們接下來將會講到。Model 不該知道關于 ViewModel 的一切。

MVVM,如果正確實現,將是一種很好的分離代碼的方式,這樣也會讓它更容易測試。它幫助我們遵循 SOLID 原則,因此我們的代碼更容易維護。

現在我將寫一個最簡單的示例來解釋它是怎樣工作的。

首先,創建一個簡單的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)

現在改變我們的布局,讓它可以觀察 textisLoading 。首先,我們會綁定 MainViewModel 而不是 Repository:

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

然后:

  • 讓 TextView 觀察 MainViewModel 中的 text

  • 添加一個 ProgressBar,當 isLoading 為 true 時才會顯示

  • 添加一個 Button,在 onClick 中調用 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.VISIBLEView.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 布局

  • activity_main.xml 布局中添加 RecyclerView

  • 創建 RepositoryRecyclerViewAdapter

  • set adapter

創建 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.
    }
}

讓我們來運行試試:


很奇怪。發生了啥?

  • Activity 被創建了,所以新的適配器也創建了,但里面的 repositories 實際上是空的

  • 我們點擊了按鈕

  • 調用了 loadRepositories 函數,顯示了進度條

  • 2 秒后,我們拿到了倉庫列表,隱藏了進度條,但倉庫列表沒顯示。因為沒有調用 notifyDataSetChanged

  • 一旦我們旋轉屏幕,新的 Activity 被創建,帶有倉庫參數的新的適配器也被創建了,所以實際上 viewModel 是有數據的。

那么,MainViewModel 該怎樣才能通知 MainActivity 更新了項目,好讓我們可以調用 notifyDataSetChanged 呢?

不應該這樣做。

這點非常重要:MainViewModel 不應該知道任何關于MainActivity的東西。

MainActivity 才擁有 MainViewModel實例,所以應該讓它來監聽數據變化并通知Adapter。那怎么做?

我們可以觀察repositories,這樣一旦數據改變了,我們就能改變我們的 adapter。

這個方案中可能出錯的地方?

我們先來看看下面的場景:

  • 在 MainActivity 中,我們觀察了 repositories:一旦改變,我們調用 notifyDataSetChanged

  • 我們點擊了按鈕

  • 當我們等待數據改變時,MainActivity 可能會因為配置改變而被重新創建

  • 我們的 MainViewModel 依然存在

  • 2 秒后,我們的 repositories 獲得新的數據,然后通知觀察者數據已經改變

  • 觀察者嘗試調用不再存在的 adapternotifyDataSetChanged,因為 MainActivity 已經重新創建了

所以,我們的方案還不夠好。

介紹 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?

  • 沒有 View 的無聊的接口,因為 ViewModel 沒有 View 的引用。

  • 沒有 Presenter 的無聊的接口,因為根本不需要。

  • 更容易處理配置改動。

  • 使用 MVVM,Activity,Fragment 里的代碼更少。

存儲庫模式

image

我之前說過,Model 是準備數據的抽象層。通常,它包括存儲和數據類。每個實體(數據)類都應該對應存儲類。例如,如果我們有個 UserPost 數據類,我們應該也有 UserRepositoryPostRepository 類。所有的數據都應該直接從它們中獲取。我們永遠不應該在 View 或者 ViewModel 中調用 Shared Preferences 或者 DB 實例。

所以,我們可以重命名我們的 RepoModel 為 GitRepoRepositoryGitRepo 從 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>)
}

MainViewModelGitRepoRepsitories 獲取 GitHub 倉庫列表,但 GitRepoRepositories 又是從哪來的呢?你可以在 repository 中調用
client 或者 DB 實例直接去拿,但這仍然不是最佳實踐。你必須盡可能地模塊化你的 app。如果你用不同的客戶端,用 Retrofit 替代 Volley 呢?如果你在里面寫了一點邏輯,你很難去重構它。你的 repository 不需要知道你正在使用哪一個客戶端來獲取遠程數據。

  • repository 需要知道的唯一一件事是數據從遠程還是本地獲取的。不需要知道我們是如何從遠程或者本地獲取。

  • ViewModel 需要的唯一一件事是數據

  • View 需要做的唯一一件事就是展示數據

我剛開始開發 Android 時,我曾經想知道應用時如何離線工作的,如何同步數據。好的應用架構允許我們讓這些變得簡單。例如,當 ViewModel 中的 loadRepositories 被調用時,如果有連接網絡,GitRepoRepositories 就能從遠程數據源中獲取數據,然后保存到本地。一旦手機處于離線模式,GitRepoRepository 就能從本地數據源獲取數據。這樣一來,Repositories 就應該有 RemoteDataSourceLocalDataSource 的實例,以及處理數據從哪里來的邏輯。

讓我們先來添加本地數據源:

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 中檢查網絡連接,這樣你就可以知道用哪個數據源獲取數據呢?我們已經說過我們不應該在 ViewModelsModels里放任何 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。

英文原文:Modern Android development with Kotlin (Part 2)
舊文推薦:
用 Kotlin 開發現代 Android 項目 Part 1
Kotlin 讓使用 Android API 變得輕松
“Effective Java” 可能對 Kotlin 的設計造成了怎樣的影響——第一部分

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

推薦閱讀更多精彩內容