使用Kotlin進行Android開發(fā)-第二部分

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

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

MVVM architecture + repository pattern + Android Manager Wrappers

請原諒我這個標題不知道怎么更清晰的描述项乒,反正主要就是MVVM架構(gòu)的東西啰劲。
關(guān)于Android世界中的機構(gòu)方面的一些東西,長期以來檀何,Android開發(fā)人員在項目中并沒有明確的架構(gòu)的東西蝇裤,在過去的三年中廷支,Android架構(gòu)如雨后春筍般的冒出來,谷歌發(fā)布了Android Architecture項目栓辜,其中包含了大量的架構(gòu)方面的示例恋拍,在Google I/O 2017上,這一系列的庫啃憎,將使我們的項目具備整潔的代碼和更好的程序結(jié)構(gòu)芝囤,你可以使用這些中的全部或者是一部分似炎,在接下來的內(nèi)容和這一系列的其他文章中辛萍,我們會使用這些庫,首先我會使用最原始的方法編碼羡藐,然后使用這些庫對代碼進行重構(gòu)贩毕,以查看這些庫可以解決哪些問題。

下面有兩種分離代碼的主要架構(gòu):

  • MVP
  • MVVM

很難說哪一種更好仆嗦,你需要嘗試過后選擇一個更適合你自己的辉阶,我給你個傾向于MVVM,使用lifecycle-aware庫瘩扼,接下來我將會使用它谆甜,如果你也沒有嘗試過MVP,medium上面有很多關(guān)于MVP的有用的文章集绰。

什么事MVVM模式

MVVM模式是一個架構(gòu)模式规辱,它代表Model-View-ViewModel,我認為這個名字會讓開發(fā)者混淆栽燕,如果我是一個為他命名的人罕袋,我會給他命名為View-ViewModel-Model,因為ViewModel是連接View和Model的橋梁碍岔。

View代表你的Activity浴讯,F(xiàn)ragment和其他Android中的自定義View的抽象,不要錯過這個View的重要性蔼啦,View應(yīng)該是愚蠢的榆纽,里面不應(yīng)該有任何邏輯相關(guān)的東西,View不應(yīng)該保存任何數(shù)據(jù)捏肢,他應(yīng)該具有ViewModel實例的引用奈籽,并且需要從ViewModel來獲取所需要的數(shù)據(jù),此外當ViewModel的數(shù)據(jù)被更改時猛计,布局也應(yīng)該被更改唠摹。

ViewModel是持有數(shù)據(jù)的類的抽象,并且具有關(guān)于核實應(yīng)該獲取數(shù)據(jù)以及核實應(yīng)該顯示數(shù)據(jù)的邏輯奉瘤,ViewModel保存當前狀態(tài)勾拉,此外煮甥,ViewModel還持有了一個或多個Model的實例,并且從中獲取所有數(shù)據(jù)藕赞,例如數(shù)據(jù)是從本地數(shù)據(jù)庫獲取還是從遠程服務(wù)器成肘,此外,ViewModel根本不用知道View斧蜕,并且ViewModel也不用去了解Android的架構(gòu)双霍。

Model是我們?yōu)閂iewModel準備數(shù)據(jù)的層,在這里我們將從遠程服務(wù)器獲取數(shù)據(jù)并將其緩存到內(nèi)存中或保存到數(shù)據(jù)庫中批销。他和那些User洒闸,Car這些之持有數(shù)據(jù)的類不一樣,通常他是一個Repository模式的實現(xiàn)均芽,接下來我們會介紹他丘逸。

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

首先深纲,讓我們創(chuàng)建一個簡單的Model類

class RepoModel {

    fun refreshData() : String {
        return "Some new data"
    }
}

通常獲取數(shù)據(jù)都是異步的,一般需要等一下劲妙,所以我們把上面的代碼做如下的改動:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }
}

interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

首先我們寫了一個OnDataReadyCallback的接口湃鹊,然后在refreshData方法中實現(xiàn)了他的方法,為了實現(xiàn)等待的效果镣奋,這里使用了Handler币呵,兩秒過后,onDataReady方法會被調(diào)用唆途。

現(xiàn)在我們來下ViewModel

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false
}

就像你看到的一樣富雅,這里有一個RepoModel的實例,text是用來顯示的肛搬,isLoading是用來保存狀態(tài)的没佑,現(xiàn)在我們加一個refresh的方法用來響應(yīng)獲取數(shù)據(jù)的動作。

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功能實際上是調(diào)用RepoModel實例的refreshData方法温赔,該實例執(zhí)行OnDataReadyCallback的方法蛤奢,每當你想實現(xiàn)一些接口或者擴展一些類而不想創(chuàng)建子類的話,你就可以使用對象申明陶贼,如果你想使用匿名內(nèi)部類的方式啤贩,這種情況下,你不許使用對象表達式

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

當我們開始刷新數(shù)據(jù)的時候拜秧,我們界面上會變成加載的狀態(tài)痹屹,當數(shù)據(jù)獲取到之后,加載的狀態(tài)就會消失枉氮。
接著志衍,我們需要把text變成ObservableField<String>,并且isLoading變成ObservableField<Boolean>,ObservableField是Data Binding庫中的一個類暖庄,用來創(chuàng)建一個可觀測的對象,它包裹了我們想要觀察的對象楼肪。

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

現(xiàn)在我們用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暂殖,用來刷新數(shù)據(jù)价匠,并且只有在isLoading是false的的狀態(tài)下才是可點擊的。
 <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}"
            />

當你運行的時候央星,你會發(fā)現(xiàn)有error霞怀,因為View.Visible和View.Gone在沒有導(dǎo)入View的時候是沒辦法直接使用的。

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

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

現(xiàn)在我們來完成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示例。
這有一個問題滞时,當你旋轉(zhuǎn)手機的時候叁幢,new data又變成了old data,這是什么情況坪稽?讓我們來看一下Activity的生命周期:


Activity生命周期
Activity生命周期

當你旋轉(zhuǎn)屏幕的時候曼玩,Activity會重新創(chuàng)建,并且onCreate方法會被調(diào)用窒百,現(xiàn)在黍判,看一下我們的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"
}

現(xiàn)在绪励,讓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()

    }
}

我們沒有創(chuàng)建新的MainViewModel的實例论咏,而是使用ViewModelProviders來獲取的,ViewModelProviders是一個獲取ViewModelProvider的工具類颁井,這個是關(guān)于范圍的厅贪。所以,如果你在Activity中調(diào)用ViewModelProviders.of(this)雅宾,那么你的ViewModel將會是活著的养涮,直到Activity被銷毀(被銷毀而不是被重新創(chuàng)建)。



ViewModelProvider負責創(chuàng)建新的實例眉抬,如果它被首次調(diào)用贯吓,或者當你重新創(chuàng)建Activity/Fragment時會返回舊實例。
不要對下面的代碼疑惑:

MainViewModel::class.java

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

MainViewModel::class

它將返回一個KClass悄谐,它與Java中的Class不同。所以库北,如果我們這樣做:.java爬舰。
在文章的最后,我說過我們會從github上獲取一個數(shù)據(jù)集并且顯示他寒瓦,所以這里我們需要寫一個getRepositories方法并且返回一個數(shù)據(jù)集合情屹。

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中添加一個方法用來調(diào)用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
            }
        })
    }
}

最后杂腰,我們需要把這些數(shù)據(jù)顯示在RecyclerView中垃你。我們需要做

  • 創(chuàng)建一個rv_item_repository.xml布局
  • 添加RecyclerView到activity_main中
  • 編寫RepositoryRecyclerViewAdapter
  • 給RecyclerView設(shè)置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按鈕出發(fā)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實現(xiàn)數(shù)據(jù)綁定。不要為下面的一行代碼困惑:

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

你可以替換參數(shù)為_煎殷,是不是很棒屯伞。
我們已經(jīng)有了Adapter,但是我們還沒有設(shè)置給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.
    }
}

運行一下豪直,會發(fā)現(xiàn)一個很奇怪的地方劣摇,數(shù)據(jù)沒有顯示,當我們旋轉(zhuǎn)屏幕弓乙,發(fā)現(xiàn)數(shù)據(jù)此時卻顯示出來了末融。

  • Activity創(chuàng)建了钧惧,adapter也創(chuàng)建了,但是repositories是空的勾习。
  • 我們點擊按鈕
  • loadRepositories方法被調(diào)用了
  • 此時repositories是有數(shù)據(jù)的浓瞪,但是我們沒有調(diào)用notifyDatasetChanged方法
  • 當我們旋轉(zhuǎn)屏幕的時候,activity重新創(chuàng)建了巧婶,adapter也重新創(chuàng)建了乾颁,但是repositories其實是已經(jīng)有數(shù)據(jù)了。
    所以MainViewModel如何通知MainActivity有新數(shù)據(jù)了艺栈,所以我們需要調(diào)用notifyDatasetchanged方法英岭。

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

MainActivity是具有MainViewModel實例的诅妹,因此它應(yīng)該是監(jiān)聽更改并通知adapter有關(guān)更改的。
但是毅人,如何做呢吭狡?
我么可以觀察repositories,一旦數(shù)據(jù)改變了堰塌,我們就可以改變我們的adapter赵刑。
讓我們看一下下面的情況:

  • 在MainActivity中,我們監(jiān)聽repositories场刑,一旦改變,我們?nèi)?zhí)行notifyDatasetChanged
  • 點擊按鈕
  • 當我們等待數(shù)據(jù)加載的時候蚪战,Activity會因為配置的改變從而導(dǎo)致重建牵现。
  • 我們的MainViewModel還是存活的狀態(tài).
  • 兩秒之后,我們獲取到了新的數(shù)據(jù)邀桑,然后通知觀察者數(shù)據(jù)改變了
  • 觀察者嘗試去調(diào)用不存在的adapter的notifyDatasetChanged方法瞎疼,因為Activity重新創(chuàng)建了。

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

介紹LiveData

LiveData是lifecycle-aware組件中的另外一個類贼急,他是基于觀察者的,可以知道View的生命周期捏萍,一旦Activity由于配置改變而銷毀的時候太抓,LiveData是知道的,所以他也會銷毀觀察者
讓我們在MainViewModel中實現(xiàn)它

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令杈,如果一些方法只有一個參數(shù)的時候走敌,該參數(shù)可以被it訪問,所以我們可以使用lambda表達式來替代:

((a) -> 2 * a) 

(it * 2)

我們再運行一下逗噩,就會發(fā)現(xiàn)一切正常了掉丽。

為什么相比MVP我更喜歡MVVM

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

Repository模式

如前所述僧须,Model只是我們準備數(shù)據(jù)的層的一個抽象名稱。通常它包含存儲庫和數(shù)據(jù)類项炼。每個實體(數(shù)據(jù))類應(yīng)該有相應(yīng)的Repository類担平。例如,如果我們有User和Post數(shù)據(jù)類芥挣,我們也應(yīng)該有UserRepository和PostRepository驱闷。所有數(shù)據(jù)都應(yīng)該直接來自它。我們不應(yīng)該從View或ViewModel調(diào)用SharePreference實例或數(shù)據(jù)庫實例空免。
所以我們可以重命名我們的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從哪來?
您可以直接在存儲庫中調(diào)用客戶端實例或數(shù)據(jù)庫實例坝咐,但這仍然不是一個好習慣循榆。您的應(yīng)用程序應(yīng)盡可能多地進行模塊化。如果您決定使用不同的客戶端墨坚,用Retrofit替換Volley秧饮?如果你有一些邏輯,那就很難重構(gòu)泽篮。您的存儲庫不需要知道您正在使用哪個客戶端來獲取遠程數(shù)據(jù)盗尸。

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

當我開始Android開發(fā)時帽撑,我想知道應(yīng)用程序如何脫機工作以及數(shù)據(jù)同步的工作原理泼各。應(yīng)用程序的良好體系結(jié)構(gòu)使我們能夠輕松實現(xiàn)這個功能。例如亏拉,當調(diào)用ViewModel中的loadRepositories時扣蜻,如果有Internet連接,GitRepoRepositories可以從遠程數(shù)據(jù)源獲取數(shù)據(jù)并將其保存在本地數(shù)據(jù)源中及塘。當手機處于脫機模式時莽使,GitRepoRepository可以從本地數(shù)據(jù)源獲取數(shù)據(jù)。所以磷蛹,Repository應(yīng)該具有RemoteDataSource和LocalDataSource的實例以及處理數(shù)據(jù)應(yīng)該來自哪里的邏輯吮旅。

添加本地數(shù)據(jù)源:

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

兩個方法,第一個是獲取數(shù)據(jù),第二個是保存數(shù)據(jù)
添加遠程數(shù)據(jù)源

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

只有一個方法就是獲取數(shù)據(jù)庇勃。
現(xiàn)在我們添加一下邏輯到我們的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>)
}

因此檬嘀,分離數(shù)據(jù)源可以輕松地在本地保存數(shù)據(jù)。 如果您只需要網(wǎng)絡(luò)中的數(shù)據(jù)责嚷,您還需要使用存儲庫模式鸳兽?是。它使您的代碼更容易測試罕拂,其他開發(fā)人員可以更好地了解您的代碼揍异,您可以更快地維護您的代碼憾股! :)

Android Manager Wrappers

如果你想檢查GitRepoRepository中的網(wǎng)絡(luò)連接問題阶捆,那么你知道從哪個數(shù)據(jù)源獲取數(shù)據(jù)嗎魄懂?我們已經(jīng)說過仍翰,我們不應(yīng)該在ViewModels和Models中放置任何Android相關(guān)的代碼,那么如何處理這個問題呢脱衙? 我們來做互聯(lián)網(wǎng)連接的包裝:

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

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

<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中創(chuàng)建實例露乏,因為我們沒有Context衍菱?我們可以在構(gòu)造函數(shù)中請求他枢舶?

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中創(chuàng)建了一個新的GitRepoRepository實例懦胞。 NetManager在ViewModel中如何才能使用NetManager?您可以使用具有上下文的Lifecycle-aware組件庫中的AndroidViewModel凉泄。這個上下文是應(yīng)用程序的上下文躏尉,而不是一個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定義構(gòu)造函數(shù)。這是必需的后众,因為AndroidViewModel在其構(gòu)造函數(shù)中要求Application實例胀糜。所以在我們的構(gòu)造函數(shù)中,我們稱之為super方法蒂誉,所以我們的類將擴展的AndroidViewModel的構(gòu)造函數(shù)被調(diào)用僚纷。
將代碼替換成一行的:

class MainViewModel(application: Application) : AndroidViewModel(application) {
... 
}

現(xiàn)在,當我們在GitRepoRepository中有NetManager的實例時拗盒,我們可以檢查互聯(lián)網(wǎng)連接:

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

所以如果我們有網(wǎng)絡(luò)連接,我們將獲得遠程數(shù)據(jù)并保存在本地锥债。另一方面陡蝇,如果我們沒有網(wǎng)絡(luò)連接,我們將獲得本地數(shù)據(jù)哮肚。

Kotlin Note:操作符let檢查可空性登夫,并在it中返回一個值.

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

資源

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末分唾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狮斗,更是在濱河造成了極大的恐慌绽乔,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碳褒,死亡現(xiàn)場離奇詭異折砸,居然都是意外死亡,警方通過查閱死者的電腦和手機沙峻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門睦授,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摔寨,你說我怎么就攤上這事去枷。” “怎么了祷肯?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵沉填,是天一觀的道長。 經(jīng)常有香客問我佑笋,道長翼闹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任蒋纬,我火速辦了婚禮猎荠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蜀备。我一直安慰自己关摇,他們只是感情好,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布碾阁。 她就那樣靜靜地躺著输虱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脂凶。 梳的紋絲不亂的頭發(fā)上宪睹,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機與錄音蚕钦,去河邊找鬼亭病。 笑死,一個胖子當著我的面吹牛嘶居,可吹牛的內(nèi)容都是我干的罪帖。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼整袁!你這毒婦竟也來了菠齿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤葬项,失蹤者是張志新(化名)和其女友劉穎泞当,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體民珍,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡襟士,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嚷量。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陋桂。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蝶溶,靈堂內(nèi)的尸體忽然破棺而出嗜历,到底是詐尸還是另有隱情,我是刑警寧澤抖所,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布梨州,位于F島的核電站,受9級特大地震影響田轧,放射性物質(zhì)發(fā)生泄漏暴匠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一傻粘、第九天 我趴在偏房一處隱蔽的房頂上張望每窖。 院中可真熱鬧,春花似錦弦悉、人聲如沸窒典。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瀑志。三九已至,卻和暖如春污秆,著一層夾襖步出監(jiān)牢的瞬間后室,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工混狠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疾层。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓将饺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子予弧,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容