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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容