【Kotlin】九、Application单例化和属性的Delegated

前言

我们很快要去实现一个数据库,如果我们想要保持我们代码的简洁性和层次性(而
不是把所有代码添加到Activity中),我们就要需要有一个更简单的访问application
context的方式。

Applicaton单例化

按照我们在Java中一样创建一个单例最简单的方式:

class App : Application() {
    companion object {
        private var instance: Application? = null
        fun instance() = instance!!
    }
    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

为了这个Application实例被使用,要记得在 AndroidManifest.xml 中增加这
个 App :

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme"
    android:name=".ui.App">
    ...
</application>

Android有一个问题,就是我们不能去控制很多类的构造函数。比如,我们不能初
始化一个非null属性,因为它的值需要在构造函数中去定义。所以我们需要一个可
null的变量,和一个返回非null值的函数。我们知道我们一直都有一个 App 实例,
但是在它调用 onCreate 之前我们不能去操作任何事情,所以我们为了安全性,我
们假设 instance() 函数将会总是返回一个非null的 app 实例。

但是这个方案看起来有点不自然。我们需要定义个一个属性(已经有了getter和
setter),然后通过一个函数来返回那个属性。我们有其他方法去达到相似的效果
么?是的,我们可以通过委托这个属性的值给另外一个类。这个就是我们知道的 委托属性 。

委托属性

我们可能需要一个属性具有一些相同的行为,使用 lazy 或者 observable 可以
被很有趣地实现重用。而不是一次又一次地去声明那些相同的代码,Kotlin提供了
一个委托属性到一个类的方法。这就是我们知道的 委托属性 。

当我们使用属性的 get 或者 set 的时候,属性委托
的 getValue 和 setValue 就会被调用。

属性委托的结构如下:

class Delegate<T> : ReadWriteProperty<Any?, T> {
    fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return ...
    }
    fun setValue(thisRef: Any?, property: KProperty<*>, value: T){
        ...
    }
}

这个T是委托属性的类型。 getValue 函数接收一个类的引用和一个属性的元数
据。 setValue 函数又接收了一个被设置的值。如果这个属性是不可修改
(val),就会只有一个 getValue 函数。

下面展示属性委托是怎么设置的:

class Example {
    var p: String by Delegate()
}

它使用了 by 这个关键字来指定一个委托对象。

标准委托

在Kotlin的标准库中有一系列的标准委托。它们包括了大部分有用的委托,但是我
们也可以创建我们自己的委托。

Lazy

它包含一个lambda,当第一次执行 getValue 的时候这个lambda会被调用,所以
这个属性可以被延迟初始化。之后的调用都只会返回同一个值。这是非常有趣的特
性, 当我们在它们第一次真正调用之前不是必须需要它们的时候。我们可以节省内
存,在这些属性真正需要前不进行初始化。

class App : Application() {
    val database: SQLiteOpenHelper by lazy {
        MyDatabaseHelper(applicationContext)
    }
    override fun onCreate() {
        super.onCreate()
        val db = database.writableDatabase
    }
}

在这个例子中,database并没有被真正初始化,直到第一次调用 onCreate 时。
在那之后,我们才确保applicationContext存在,并且已经准备好可以被使用
了。 lazy 操作符是线程安全的。

如果你不担心多线程问题或者想提高更多的性能,你也可以使
用 lazy(LazyThreadSafeMode.NONE){ ... } 。

Observable

这个委托会帮我们监测我们希望观察的属性的变化。当被观察属性的 set 方法被
调用的时候,它就会自动执行我们指定的lambda表达式。所以一旦该属性被赋了新
的值,我们就会接收到被委托的属性、旧值和新值。

class ViewModel(val db: MyDatabase) {
    var myProperty by Delegates.observable("") {d, old, new ->db.saveChanges(this,new)}
}

这个例子展示了,一些我们需要关心的ViewMode,每次值被修改了,就会保存它
们到数据库。

Vetoable

这是一个特殊的 observable ,它让你决定是否这个值需要被保存。它可以被用
于在真正保存之前进行一些条件判断。

var positiveNumber = Delegates.vetoable(0) {d, old, new ->new >= 0}

上面这个委托只允许在新的值是正数的时候执行保存。在lambda中,最后一行表示
返回值。你不需要使用return关键字(实质上不能被编译)。

Not Null

有时候我们需要在某些地方初始化这个属性,但是我们不能在构造函数中确定,或
者我们不能在构造函数中做任何事情。第二种情况在Android中很常见:在
Activity、fragment、service、receivers……无论如何,一个非抽象的属性在构造函
数执行完之前需要被赋值。为了给这些属性赋值,我们无法让它一直等待到我们希
望给它赋值的时候。我们至少有两种选择方案。

第一种就是使用可null类型并且赋值为null,直到我们有了真正想赋的值。但是我们
就需要在每个地方不管是否是null都要去检查。如果我们确定这个属性在任何我们
使用的时候都不会是null,这可能会使得我们要编写一些必要的代码了。

第二种选择是使用 notNull 委托。它会含有一个可null的变量并会在我们设置这个
属性的时候分配一个真实的值。如果这个值在被获取之前没有被分配,它就会抛出
一个异常。

这个在单例App这个例子中很有用:

class App : Application() {
    companion object {
        var instance: App by Delegates.notNull()
    }
    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

从Map中映射值

另外一种属性委托方式就是,属性的值会从一个map中获取value,属性的名字对应
这个map中的key。这个委托可以让我们做一些很强大的事情,因为我们可以很简
单地从一个动态地map中创建一个对象实例。如果我们import
kotlin.properties.getValue ,我们可以从构造函数映射到 val 属性来得到
一个不可修改的map。如果我们想去修改map和属性,我们也可以import
kotlin.properties.setValue 。类需要一个 MutableMap 作为构造函数的参
数。

想象我们从一个Json中加载了一个配置类,然后分配它们的key和value到一个map
中。我们可以仅仅通过传入一个map的构造函数来创建一个实例:

import kotlin.properties.getValue

class Configuration(map: Map<String, Any?>) {
    val width: Int by map
    val height: Int by map
    val dp: Int by map
    val deviceName: String by map
}

作为一个参考,这里我展示下对于这个类怎么去创建一个必须要的map:

conf = Configuration(mapOf(
    "width" to 1080,
    "height" to 720,
    "dp" to 240,
    "deviceName" to "mydevice"
))

怎么去创建一个自定义的委托

先来说说我们要实现什么,举个例子,我们创建一个 notNull 的委托,它只能被
赋值一次,如果第二次赋值,它就会抛异常。

Kotlin库提供了几个接口,我们自己的委托必须要实
现: ReadOnlyProperty 和 ReadWriteProperty 。具体取决于我们被委托的对
象是 val 还是 var 。

我们要做的第一件事就是创建一个类然后继承 ReadWriteProperty :

private class NotNullSingleValueVar<T>() : ReadWriteProperty<Any
?, T> {
override fun getValue(thisRef: Any?, property: KProperty
<*>): T {
throw UnsupportedOperationException()
}
override fun setValue(thisRef: Any?, property: KProperty
<*>, value: T) {
}
}

这个委托可以作用在任何非null的类型。它接收任何类型的引用,然后像getter和
setter那样使用T。现在我们需要去实现这些函数。

  • Getter函数 如果已经被初始化,则会返回一个值,否则会抛异常。
  • Setter函数 如果仍然是null,则赋值,否则会抛异常。
private class NotNullSingleValueVar<T>() : ReadWriteProperty<Any
?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>)
: T {
return value ?: throw IllegalStateException("${desc.name
} " +
"not initialized")
}
override fun setValue(thisRef: Any?, property: KProperty<*>,
value: T) {
this.value = if (this.value == null) value
else throw IllegalStateException("${desc.name} already i
nitialized")
}
}

现在你可以创建一个对象,然后添加函数使用你的委托:

object DelegatesExt {
    fun notNullSingleValue<T>():
        ReadWriteProperty<Any?, T> = NotNullSingleValueVar()
}

重新实现Application单例化

在这个情景下,委托就可以帮助我们了。我们直到我们的单例不会是null,但是我
们不能使用构造函数去初始化属性。所以我们可以使用 notNull 委托:

class App : Application() {
    companion object {
        var instance: App by Delegates.notNull()
    }
    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

这种情况下有个问题,我们可以在app的任何地方去修改这个值,因为如果我们使
用 Delegates.notNull() ,属性必须是var的。但是我们可以使用刚刚创建的委
托,这样可以多一点保护。我们只能修改这个值一次:

companion object {
    var instance: App by DelegatesExt.notNullSingleValue()
}

尽管,在这个例子中,使用单例可能是最简单的方法,但是我想用代码的形式展示
给你怎么去创建一个自定义的委托。

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

推荐阅读更多精彩内容

  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,139评论 9 118
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,590评论 18 139
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,521评论 0 5
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,565评论 18 399
  • java分布式应用如何入门
    尘世的鱼阅读 225评论 0 2