【译】使用Kotlin从零开始写一个现代Android 项目-Part2

3,813 阅读19分钟

接着上一篇文章:使用Kotlin从零开始写一个现代Android 项目-Part1

5. MVVM架构+Repository模式+Android Manager

5.1 关于Android中的架构

长期以来,Android开发的项目中很少有架构,但是在过去几年,架构在各大Android社区广泛宣传。Activity即一切的时代过去了,Google发布了一个仓库叫做Android Architecture Blueprints,它包含了许多示例和不同架构的说明。最后,在Google IO/17大会上,发布了Android Architecture Components系列架构组件,可以帮助我们写更简洁、高质量的应用程序。你可以使用一个全部组件或者其中一个来构建你的应用程序,不过,我发现它们都非常有用,因此,本文剩下的部分和后面2部分中,我将介绍如何使用这些组件。首先,我将写一些有问题的代码,然后使用这些组件来重构,以看看这些库能帮我们解决什么问题。

这里主要有两种架构模式

  • MVP
  • MVVM

很难说它两谁更好,你应该都试试以后再决定。我个人更喜欢带有生命周期组件的MVVM架构,本系列将围绕它来介绍,如果你还没有使用过MVP架构,Medium上有很多关于它的好文章,你可以去看看。

5.2 什么是MVVM模式?

MVVM模式是一种架构模式。它代表Model-View-ViewModel。我认为这个名称会使开发人员感到困惑。如果我来命名它的话,我会将其命名为View-ViewModel-Model,因为ViewModel是连接View和Model的中间人。

其中View是对你的Activity/Fragment/或者其他自定义View的抽象名称,请注意,不要将它与Android 的View混为一谈,这非常重要。View应该是干净的,在View中,不应该包含任何逻辑代码,也不应该持有任何数据,他应该持有一个ViewModel实例,所有的数据都应该从实例中去获取。此外,View应该观察这些数据,并且当ViewModel中的数据更改时,布局也应该刷新一次。总之,View的职责是:布局如何查找不同的数据和状态。

ViewModel是保存数据的类的抽象名称,并具有何时应获取数据以及应何时显示数据的逻辑。 ViewModel保持当前状态。此外,ViewModel应该保持一个或者多个Model实例,所有的数据都应该从这些Model实例获取。例如,ViewModel不应该知道数据是来自数据库还是远程服务器。此外,ViewModel完全不应该了解View。而且,ViewModel也完全不应该了解Android框架层的东西。

Model是数据层的抽象名称。这是我们将从远程服务器获取数据并将其缓存在内存中或保存在本地数据库中的类。但是请注意,这里的Model和CarUserSquare 这些model类是不一样,这些数据模型类仅仅只保持数据,而Model是Repository模式的实现,在后文将介绍,并且Model不应该了解ViewModel。

如果正确实施,MVVM是分离代码并使其更具可测试性的好方法。它有助于我们遵循SOLID原则,因此我们的代码更易于维护。

代码示例

现在,我将写一个最简单的例子来说明它是如何工作的

首先,让我们创建一个简单的Model,该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,然后将OnDataReadyCallback作为refreshData的参数,用Handler来模拟等待,当2000ms后,调用接口实例的onDataReady方法。

让我们看一下ViewModel:

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

如你所见,这里有一个RepoModel实例,一个我们要显示的 text,和一个保存状态的boolean值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时,我们应该将视图更改为加载状态,一旦数据到来,就应该将isLoading设置为false

另外,我们应该将text更改为ObservableField <String>,并将isLoading更改为ObservableField <Boolean>。 ObservableField是Data Binding库中的一个类,我们可以使用它代替创建Observable对象。它包装了我们想要观察的对象。

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.mladenrakonjac.modernandroidapp.MainViewModel" />
</data>

然后,做一下操作:

  • 更改TextView以观察MainViewModel实例上的text
  • 添加仅在isLoadingtrue时可见的ProgressBar
  • 单击的add按钮将从MainViewModel实例调用refresh函数,并且仅在isLoadingfalse时才可单击
...

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

ok,布局就到此完成,接下来该完成绑定了,如我们所说,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()

    }
}

最后,我们可以运行它了。

您可以看到旧数据已更改为新数据

这是最简单的MVVM示例。

对此有一个问题,让我们现在旋转手机:

新数据又变回了旧数据。这怎么可能呢?看一下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实例,那会很好吗?

Lifecycle-aware 组件介绍

由于许多开发人员都遇到了这个问题,因此Android Framework Team的开发人员决定开发可帮助我们解决这个问题的库。 ViewModel类就是其中之一。它是我们所有ViewModels都应该扩展的类。

让我们的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"
}

MainViewModel继承自ViewModel,如下:

package me.mladenrakonjac.modernandroidapp

import android.arch.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    ...
}

在Activity的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实例,我们从ViewModelProvider中获取它,ViewModelProviders是一个工具类,它有获取ViewModel实例的方法。与范围相关,如果你在Activity中调用ViewModelProviders.of(this),则ViewModel会一直存在,知道Activity被彻底销毁(销毁没有被重建),同样的,如果你在Fragment中调用,ViewModel会一直存在,直到Fragment被彻底销毁。看看下图:

ViewModelProvider负责在第一次调用时创建新实例,或在重新创建Activity / Fragment时返回旧实例。

请勿与以下内容混淆:

MainViewModel::class.java

在Kotlin中,如果你向下面这样写:

MainViewModel::class

它将返回一个KClass,它与Java中的Class不同。因此,我们需要加一个.java后缀。

返回与给定KClass实例相对应的Java Class实例。

让我们看一下,旋转屏幕会发生什么?

我们拥有与之前旋转时相同的数据。

在上一篇文章中,我说过我们的应用程序将获取Github仓库列表并显示它。为此,我们必须添加getRepositories函数,该函数将返回mock的仓库列表:

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

与之对应,在ViewModel中,也应该有一个函数,该函数调用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布局
  • activity_main.xml添加RecyclerView
  • 添加RepositoryRecyclerViewAdapter适配器
  • 给RecycklerView设置Adapter

为了让rv_item_repository.xml使用CardView,需要在build.gradle中添加(译者注:最新的请使用androidx):

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.mladenrakonjac.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>

下一步是将RecyclerView添加到main_activity.xml,在这之前,别忘了添加:

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函数,因为我们不再需要它们了。

现在,我们添加一个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类型,这样我们就可以在Item中使用Data Binding了,另外,不要被下面这行代码搞迷惑了:

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秒后,我们得到了repositories,进度被隐藏了,但是列表却没有显示。这是因为未在适配器上调用notifyDataSetChanged
  • 旋转屏幕后,将创建新的Activity,因此将使用带有一些内容的repositories参数创建新的适配器

因此,MainViewModel应该如何通知MainActivity有关新Item的信息,以便我们可以调用notifyDataSetChanged

这不应该是ViewModel来做的

这非常重要,MainViewModel应该了解MainActivity,MainActivity是拥有MainViewModel实例的,因此它是应该侦听更改并通知Adapter有关更改。

那?到底该如何做呢?

我们要可以观察repositories,当他改变的时候,通知列表更新。

该解决方案有什么问题没?

我们看一下以下场景:

  • 在MainActivity中观察repositories,一旦它放生更改,我们调用notifyDataSetChanged
  • 我们点击按钮
  • 在我们等待数据更改时,由于配置更改,可以重新创建MainActivity。
  • 但我们的MainViewModel仍然存在。
  • 2秒后,“repositories”字段获取新项目,并通知观察者数据已更改
  • 观察者尝试对不再存在的适配器执行notifyDataSetChanged,因为重新创建了MainActivity。

然后程序就崩了,因此上面的方案不够好。我们得引入一个新的组件你LiveData

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关键字代表什么呢?在Kotlin中,如果,函数只有一个参数,那么它默认你会被替换成it,假设我们有一个乘以2的lamuda表达式:

((a) -> 2 * a) 

可以写成下面这样 :

(it * 2)

现在你运行程序,一切都正常工作了!

我为什么喜欢MVVM而不是MVP

  • 没有那些提供给View的无聊接口,因为ViewModel没有对View的引用
  • 也没有提供给Presenter的接口,因为不需要
  • 它处理配置更改是如此简单
  • 使用MVVM,我们Activity/Fragment中的代码更简洁

Repository 模式

正如我前面所说,Model只是数据层的一个抽象,通常来说,它包含repositories和数据类,每一个实体(data)类应该有一个对应的Repository类。例如,我们有一个User和一个Post数据类,那么也应该对应有UserRepository和PostRepository,所有的数据都应该从Repository处获取。我们不应该在View或者ViewModel中直接使用Shared Preferences或者DB。

因此,我们可以将RepoModel重命名为GitRepoRepository,其中GitRepo来自Github存储库,而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从GitRepoRepsitories获取Github 仓库列表,但是GitRepoRepositories数据从何而来?

你可以直接在Repository中对客户端实例或数据库实例进行调用,但这仍然不是一个好习惯。你的应用程序应尽可能模块化。如果你决定使用其他Client,用Retrofit代替Volley怎么办?如果你有一些逻辑在里面,将很难对其进行重构。你的存储库不需要知道你要使用哪个客户端来获取远程数据。

  • repository 仅需要知道数据是来自远程还是本地,而不需要知道是如何从远程或者本地获取
  • ViewModel仅只需要数据
  • View 仅需要显示数据

当我刚开始开发Android的时候,我就在想,APP如何在离线情况下工作?数据同步是怎样实现的?好的架构是我们很容易做到这些。比如,当loadRepositories在ViewModel中被调用的时候,如果网络链接正常,GitRepoRepositories从远程获取数据,并且将数据保存到本地数据源,一旦手机处于离线模式,GitRepoRepositories可以从本地数据源获取数据,因此,Repositories需要有持有远程数据源实例RemoteDataSource和本地数据源实例LocalDataSource和处理数据从哪里来的逻辑。

让我们添加一个本地数据源(local data source):

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

因此,分离数据源,我们可以轻松地在本地保存数据。

如果你只需要来自网络的数据该怎么办》?还需要使用Repository模式吗?是。它使您的代码更易于测试,其他开发人员可以更好地理解您的代码,并且可以更快地对其进行维护! :)

Android Manager 包装器

如果要在GitRepoRepository中检查Internet连接,以便可以知道要查询哪个数据源,该怎么办?我们已经说过,我们不应该在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" />

但是,怎么在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呢?因为我们需要给NetManager传一个Context,你可以使用生命周期感知组件中的AndroidViewModel,它带有Contenxt,它的Context是一个Application Context而不是一个Activity的Context。

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实例。因此,在我们的构造函数中,需要调用AndroidViewModel的构造函数的super方法,以便可以调用我们继承类的构造函数。

注意:我们可以简写成一行:

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运算符可以检查可控性,并且从其中返回一个值

预告

在后面的文章中,我将介绍依赖项注入,为什么在ViewModel中创建存储库实例很糟糕,以及如何避免使用AndroidViewModel。另外,到目前为止,写的代码中有一些问题,这样写是有原因的,我试图让你面对这些问题,以便你可以理解为什么所有这些库都很受欢迎以及为什么要使用它。

最后,感谢你的阅读!

本系列已更新完毕:

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

【译】使用Kotlin从零开始写一个现代Android 项目-Part3

【译】使用Kotlin从零开始写一个现代Android 项目-Part4

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你