前言

最近项目中有用到LiveData+ViewModel的架构组件,今天来学习一波。本篇文章参考:MVVM 架构,ViewModel和LiveData
所有语言为Kotlin。


LiveData

LiveData是一个可以被观察的数据持有者.这也就意味着应用中的组件能够观察LiveData对象的更改,而无需在它们之间创建明确的和严格的依赖关系。这将完全分离LiveData对象使用者和LiveData对象生产者。
除此之外,LiveData还有一个很大的好处,LiveData遵守应用程序组件(活动,片段,服务)的生命周期状态,并进组件的生命周期管理,确保LiveData对象的内存泄漏。
注:上述的遵守应用程序组件的生命周期是指,譬如说Activity的生命周期在onStart、onResume的状态下是可以接收到LiveData内的数据变化回调的。而在Stop之后数据变化不会进行回调。而且在Activity销毁的时候,LiveData也会随之销毁,简单理解为它和Activity的生命周期类似,这也防止了内存泄漏。

在开发中我们多用的是继承LiveData的子类MutableLiveData<T>。里面还有两个方法,setValue和postValue,setValue必须在主线程调用。postValue可以在后台线程中调用。


ViewModel

ViewModel是和Model(数据层)进行交互,并且ViewMode可以被View观察.ViewModel可以选择性地为视图提供钩子以将事件传递给模型.该层的一个重要实现策略是将Model与View分离,即ViewModel不应该意识到与谁交互的视图。

通俗地来说,ViewModel所承担的职责就是UI层与数据层的中间层。

  • 一方面可以减轻View层的逻辑,View层可以更好的针对UI做设置而无需考虑其他逻辑。
  • 另一方面ViewModel作为View和Model的中间层,将View和Model解耦合。

此外,ViewModel还有一个比较厉害的功能,通常情况下activity会被系统自动销毁,此时我们用于保存数据的方式就是事先在onSaveInstanceState()方法中规定好需要存储的数据。但是这种做法有一个缺点就是保存的数据要经过序列化。使用ViewModel的话ViewModel会自动保留之前的数据并给新的Activity或Fragment使用。直到当前Activity被系统销毁时,Framework会调用ViewModel的onCleared()方法,我们可以在onCleared()方法中做一些资源清理操作。


使用ViewModel+LiveData编写一个简易demo

1、首先我们需要编写一个实体类,用于数据的封装:

`data class AccountBean(

    val name:String = "",
    val number:String = "")`

2、创建一个自定义的ViewModel:

`class AccountModel : ViewModel() {
    val accountLiveData = MutableLiveData<AccountBean>()

    fun getData(){
        accountLiveData.value = AccountBean("cyber","2077")
    }

    override fun onCleared() {
        super.onCleared()
    }
}`

在这个ViewModel里面我们定义了一个MutableLiveData的对象其本质就是一个LiveData,而这个LiveData里的数据绑定的是刚刚我们定义的实体类型AccountBean。
getData方法是用于模拟数据更新的,当调用此方法时,我们会设置上述的LiveData里的数据内容。上述的accountLiveData.value实际上是调用了MutableLiveData里的setValue方法。

3、View层的编写
接下来就是View层了,其实就是Activity层

xml:

`<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:text="Hello World!"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="217dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text" />

</android.support.constraint.ConstraintLayout>`

kotlin:

`class MainActivity : AppCompatActivity() {

    private lateinit var mAccountModel:AccountModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //初始化ViewModel
        mAccountModel= ViewModelProviders.of(this).get(AccountModel::class.java)
        //点击按钮后更新LiveData的数据实体
        button.setOnClickListener{
            mAccountModel.getData()
        }
        //设置LiveData的观察者相应更新TextView
        mAccountModel.accountLiveData.observe(this, Observer {
            text.setText("名字:${it!!.name},号码:${it!!.number}")
        })
    }
}`

界面很简单,就是一个TextView和一个Button。
首先我们需要将刚刚写好的ViewModel作为对象创建出来然后再onCreate时候初始化,这里需要注意的是Kotlin默认定义对象时需要初始化,所以这里使用lateinit关键字来声明对象需要延迟初始化。
点击按钮的时候,我们模拟数据更新调用刚刚在自定义ViewModel中的getData方法更新LiveData里的数据。
此处我们注册了LiveData的观察模式,当数据发生变化时会回调上述的observe方法,而在Observer对象中其实是有一个onChange方法的,上述的it代表的其实就是刚刚改变的那个数据(AccountBean对象)。

构造方法

Kotlin中的类构造方法可以类似java中的编写:

class Rect{
    var h:Float;
    var w:Float;

    constructor(h: Float,w: Float) {
        this.h = h
        this.w = w
    }
}

上述定义了一个矩形类,里面有两个属性(长h、宽w)。constructor为其构造函数的声明关键字。这里要注意的是,在Kotlin中定义属性时需要给该属性赋初值,或者需要一个拥有该属性赋值的构造方法。 否则程序会出错。

也可以这样写:

class Rect(var h: Float,var w: Float){

}

这种写法可以既定义类的属性又声明了构造方法。
关于构造函数详细的写法可以参考:Kotlin中的构造函数


继承

Kotlin中的继承声明为 class 子类名:父类名{}

class Square : Rect(){

}

这里有一个父类Rect,一个子类Square。需要注意的是,父类需要声明open关键字,才可以被子类继承。

open class Rect{

}

在方法的重写上,子类需要声明override关键字来声明此方法为重写父类的方法。相应的父类也需要在对应的方法中声明open关键字

open class Rect{
    open fun S(){

    }
}

class Square : Rect(){
    override fun S(){

    }
}

抽象类

Kotlin的抽象类定义与java类似,接下来直接上代码:

abstract class Animal(var name: String){
    abstract fun type();
}

class Dog(name: String):Animal(name){
    override fun type() {
        
    }
}

使用abstract关键字声明Animal类为一个抽象类,内含一个type的抽象方法。Dog类在继承它的时候自动重写此type方法,并需要加上override的关键字。


接口定义与继承

Kotlin的接口定义和继承与java写法大致,以下是子类继承了父类和一个接口的例子:

abstract class Animal(var name: String){
    abstract fun type();
}

interface Ability{
    fun shout();
}

class Dog(name: String):Animal(name),Ability{
    override fun shout() {
        
    }

    override fun type() {

    }
}

is关键字

在java的多态中我们可以使用一个父类的对象通过instanceof关键字来判断其是否为某个子类的引用。而类似的在Kotlin里也可以使用is关键字来进行判断。

fun main(args: Array<String>) {
    var a: Animal = Dog("Boby");
    if(a is Dog){
        println(a.name);
    }
}

输出结果:

Boby

代理与委托

Kotlin可以使用by关键字实现代理模式,详情可见:kotlin 委托


单例

Kotlin可以利用object关键字声明一个类为单例模式。

object SignalInstance{

}

我们可以试着定义两个变量都为SignalInstance的。

var b = SignalInstance;
var c = SignalInstance;
println(b == c);

输出结果:

true

枚举型

Kotlin中通过enum class 关键字定义一个枚举型。如:

enum class NUMBER{
    ONE,TWO,THREE,FOUR,FIVE
}

通过类名.属性的格式可以获取该属性:

println(NUMBER.ONE);

输出结果:

ONE

可通过ordinal关键字获取此属性在枚举中的位置:

println(NUMBER.ONE.ordinal);

输出结果:

0

印章类

sealed关键字可以定义一个印章类,印章类让一个类拥有了有限多个子类。印章类甚至可以理解为一个特殊的枚举类。印章类本身不能被实例化。

sealed class Option(var num1:Int,var num2:Int){
    class add(num1: Int,num2: Int):Option(num1,num2){
        fun add() = num1+num2;
    }

    class sub(num1: Int,num2: Int):Option(num1,num2){
        fun sub() = num1-num2;
    }
}

在main函数中定义如下变量会出错:

 var x : Option = Option(1,1);

这是因为Option类为印章类,其本身不可被实例化。

以下是一段使用印章类的代码:

fun main(args: Array<String>) {
    var a : Option = Option.Add(2,3);
    var b : Option = Option.Sub(3,2);
    var list = listOf<Option>(a,b);
    for (o in list){
        if(o is Option.add){
           println(o.add());
        }else if(o is Option.sub){
            println(o.sub());
        }
    }
}

上述通过定义在Option的印章类中两个子类的对象,在通过调用其子类中的不通方法完成输出。
印章类可定义有限的子类,每个子类中都有其自定义的内容,这是与枚举相比的最大区别。

函数的编写规范

fun 函数名 (参数名 : 参数类型 ) : 返回值类型 (

函数体

注:返回值类型若无返回值则声明Unit或者忽略不写,相当于java中的void

编写一个简单的加法函数:

fun add(x:Int,y:Int):Int{
    return x+y;
}

函数的简单写法

如果函数里只包含一行代码,我们可以使用简单写法。上述的加法函数作变形:

fun add(x:Int,y:Int):Int = x+y;

函数表达式

Kotlin还可以使用var来定义一个函数,譬如:

var i = {x:Int,y:Int -> x+y};

这样定义出来的函数,其函数名为i。调用如下:

var a = i(10,20);

除此之外还有另外一种写法:

var j:(Int,Int) -> Int = {x,y -> x+y};

这样定义出来的函数,函数名为j。和第1种的效果一样。


默认参数与具名参数

有时候我们在调用函数时,部分参数不需要设置,而是有一个默认值。譬如计算圆周长时的pi。这时我们就可以通过设置默认参数实现。

fun main(args: Array<String>) {
    C(r=2.0f);
}

val PI = 3.14f;

fun C(pi:Float = PI,r:Float):Float{
    return 2*pi*r;
}

我们定义了一个常量(val)PI,在声明函数C时在其参数中设置pi(半径)有一个默认值为PI。在调用时,则需要显示的声明所需要传递的参数值。如:C(r=2.0f); 就是表明我们传递过去的是半径r。这种情况叫做具名参数。

when表达式

when表达式的用法类似与java中的switch表达式

fun main(args: Array<String>) {
    log(1);
    log(2);
    log(3);
    log(4);
    log(5);
    log(6);
}

fun log(logLevel: Int){
    when(logLevel){
        1 -> println(logLevel);
        2 -> println(logLevel);
        3 -> println(logLevel);
        4 -> println(logLevel);
        5 -> println(logLevel);
        else -> println("else");
    }
}

输出结果:

1
2
3
4
5
else

Range

可以在Kotlin中定义一个区间,以下是先定义一个1到100的闭区间,然后通过循环计算出1到100之和的代码:

var nums = 1..100;
var sum = 0;
for(num in nums){
    sum+=num;
}
println(sum);

注:这里插一个题外话,上述的for(num in nums)与java中的foreach循环相类似。

开闭区间的表示方法:

  • [1,100]:1 .. 100
  • [1,100):1 until 100

将集合倒叙api :nums.reversed();
获取集合总数:nums.count();

循环的写法(loop)

  • for(num in nums)
    上述提到的for(num in nums)与java中的foreach循环类似,nums可看作一个集合,num为遍历过程中的某一个子项。
  • for(num in nums step 2)
    在第一点的基础上加上step i,i为所要设置的步长,即遍历是从第0个开始隔i个获取一次值:
    fun main(args: Array<String>) {
            var nums = 1..10;
            for(num in nums step 2){
                println(num);
            }
    }
    

输出结果:

    1
    3
    5
    7
    9

List和Map的创建

List和Map与java中的不同在于它们的创建写法,其余的api与java类似。有需要可查阅此处:常用集合的使用

  • List的创建

    var nums = listOf< String >(
    "111",
    "222",
    "333",
    "444");

  • Map的创建与添加

    var map = HashMap< String,String >();
    map["one"]="a";
    map["two"]="b";

    注:对于map的取值只需要map[对应的key值],如:map["one"]