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的时候,属性委托的getValuesetValue就会被调用。

属性委托的结构如下:

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库提供了几个接口,我们自己的委托必须要实现:ReadOnlyPropertyReadWriteProperty。具体取决于我们被委托的对象是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 initialized")
    }
}

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

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

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

创建一个SQLiteOpenHelper

如你所知,Android使用SQLite作为它的数据库管理系统。SQLite是一个嵌入app的一个数据库,它的确是非常轻量的。这就是为什么这是手机app的不错的选择。

尽管如此,它的操作数据库的API在Android中是非常原生的。你将会需要编写很多SQL语句和你的对象与ContentValues或者Cursors之间的解析过程。很感激的,联合使用Kotlin和Anko,我们可以大量简化这些。

当然,有很多Android中可以使用的关于数据库的库,多亏Kotlin的互操作性,所有这些库都可以正常使用。但是针对一个简单的数据库来说可以不使用任何它们,之后的一分钟之内你就可以看到。

ManagedSqliteOpenHelper

Anko提供了很多强大的SqliteOpenHelper来可以大量简化代码。当我们使用一个一般的SqliteOpenHelper,我们需要去调用getReadableDatabase()或者getWritableDatabase(),然后我们可以执行我们的搜索并拿到结果。在这之后,我们不能忘记调用close()。使用ManagedSqliteOpenHelper我们只需要:

forecastDbHelper.use {
    ...
}

在lambda里面,我们可以直接使用SqliteDatabase中的函数。它是怎么工作的?阅读Anko函数的实现方式真是一件有趣的事情,你可以从这里学到Kotlin的很多知识:

public fun <T> use(f: SQLiteDatabase.() -> T): T {
    try {
        return openDatabase().f()
    } finally {
        closeDatabase()
    }
}

首先,use接收一个SQLiteDatabase的扩展函数。这表示,我们可以使用this在大括号中,并且处于SQLiteDatabase对象中。这个函数扩展可以返回一个值,所以我们可以像这么做:

val result = forecastDbHelper.use {
    val queriedObject = ...
    queriedObject
}

要记住,在一个函数中,最后一行表示返回值。因为T没有任何的限制,所以我们可以返回任何对象。甚至如果我们不想返回任何值就使用Unit

由于使用了try-finallyuse方法会确保不管在数据库操作执行成功还是失败都会去关闭数据库。

而且,在sqliteDatabase中还有很多有用的扩展函数,我们会在之后使用到他们。但是现在让我们先定义我们的表和实现SqliteOpenHelper

定义表

创建几个objects可以让我们避免表名列名拼写错误、重复等。我们需要两个表:一个用来保存城市的信息,另一个用来保存某天的天气预报。第二张表会有一个关联到第一张表的字段。

CityForecastTable提供了表的名字还有需要列:一个id(这个城市的zipCode),城市的名称和所在国家。

object CityForecastTable {
    val NAME = "CityForecast"
    val ID = "_id"
    val CITY = "city"
    val COUNTRY = "country"
}

DayForecast有更多的信息,就如你下面看到的有很多的列。最后一列cityId,用来保持属于的城市id。

object DayForecastTable {
    val NAME = "DayForecast"
    val ID = "_id"
    val DATE = "date"
    val DESCRIPTION = "description"
    val HIGH = "high"
    val LOW = "low"
    val ICON_URL = "iconUrl"
    val CITY_ID = "cityId"
}

实现SqliteOpenHelper

我们SqliteOpenHelper的基本组成是数据库的创建和更新,并提供了一个SqliteDatebase,使得我们可以用它来工作。查询可以被抽取出来放在其它的类中:

class ForecastDbHelper() : ManagedSQLiteOpenHelper(App.instance,
        ForecastDbHelper.DB_NAME, null, ForecastDbHelper.DB_VERSION) {
    ...
}

我们在前面的章节中使用过我们创建的App.instance,这次我们同样的包括数据库名称和版本。这些值我们都会与SqliteOpenHelper一起定义在companion object中:

companion object {
    val DB_NAME = "forecast.db"
    val DB_VERSION = 1
    val instance: ForecastDbHelper by lazy { ForecastDbHelper() }
}

instance这个属性使用了lazy委托,它表示直到它真的被调用才会被创建。用这种方法,如果数据库从来没有被使用,我们没有必要去创建这个对象。一般lazy委托的代码块可以阻止在多个不同的线程中创建多个对象。这个只会发生在两个线程在同事时间访问这个instance对象,它很难发生但是发生具体还有看app的实现。无人如何,lazy委托是线程安全的。

为了去创建这些定义的表,我们需要去提供一个onCreate函数的实现。当没有库使用的时候,创建表会通过我们编写原生的包含我们定义好的列和类型的CREATE TABLE语句来实现。然而Anko提供了一个简单的扩展函数,它接收一个表的名字和一系列由列名和类型构建的Pair对象:

db.createTable(CityForecastTable.NAME, true,
        Pair(CityForecastTable.ID, INTEGER + PRIMARY_KEY),
        Pair(CityForecastTable.CITY, TEXT),
        Pair(CityForecastTable.COUNTRY, TEXT))
  • 第一个参数是表的名称
  • 第二个参数,当是true的时候,创建之前会检查这个表是否存在。
  • 第三个参数是一个Pair类型的vararg参数。vararg也存在在Java中,这是一种在一个函数中传入联系很多相同类型的参数。这个函数也接收一个对象数组。

Anko中有一种叫做SqlType的特殊类型,它可以与SqlTypeModifiers混合,比如PRIMARY_KEY+操作符像之前那样被重写了。这个plus函数会把两者通过合适的方式结合起来,然后返回一个新的SqlType

fun SqlType.plus(m: SqlTypeModifier) : SqlType {
    return SqlTypeImpl(name, if (modifier == null) m.toString()
            else "$modifier $m")
}

如你所见,她会把多个修饰符组合起来。

但是回到我们的代码,我们可以修改得更好。Kotlin标准库中包含了一个叫to的函数,又一次,让我们来展示Kotlin的强大之处。它作为第一参数的扩展函数,接收另外一个对象作为参数,把两者组装并返回一个Pair

public fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

因为带有一个函数参数的函数可以被用于inline,所以结果非常清晰:

val pair = object1 to object2

然后,把他们应用到表的创建中:

db.createTable(CityForecastTable.NAME, true,
        CityForecastTable.ID to INTEGER + PRIMARY_KEY,
        CityForecastTable.CITY to TEXT,
        CityForecastTable.COUNTRY to TEXT)

这就是整个函数看起来的样子:

override fun onCreate(db: SQLiteDatabase) {
    db.createTable(CityForecastTable.NAME, true,
            CityForecastTable.ID to INTEGER + PRIMARY_KEY,
            CityForecastTable.CITY to TEXT,
            CityForecastTable.COUNTRY to TEXT)

    db.createTable(DayForecastTable.NAME, true,
            DayForecastTable.ID to INTEGER + PRIMARY_KEY + AUTOINCREMENT,
            DayForecastTable.DATE to INTEGER,
            DayForecastTable.DESCRIPTION to TEXT,
            DayForecastTable.HIGH to INTEGER,
            DayForecastTable.LOW to INTEGER,
            DayForecastTable.ICON_URL to TEXT,
            DayForecastTable.CITY_ID to INTEGER)
}

我们有一个相似的函数用于删除表。onUpgrade将只是删除表,然后重建它们。我们只是把我们数据库作为一个缓存,所以这是一个简单安全的方法保证我们的表会如我们所期望的那样被重建。如果我有很重要的数据需要保留,我们就需要优化onUpgrade的代码,让它根据数据库版本来做相应的数据转移。

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    db.dropTable(CityForecastTable.NAME, true)
    db.dropTable(DayForecastTable.NAME, true)
    onCreate(db)
}

依赖注入

我试图不去增加很复杂的结构代码,保持简洁可测试性的代码和好的实践,我想我应该用Kotlin从其它方面去简化代码。如果你想了解一些控制反转或者依赖注入的话题,你可以查看我关于Android中使用Dagger注入的一系列文章。第一篇文章有关于他们这个团队的简单描写。

一种简单的方式,如果我们想拥有一些独立于其他类的类,这样更容易测试,并编写代码,易于扩展和维护,这时我们需要使用依赖注入。我们不去在类内部去实例化,我们在其它地方提供它们(通常通过构造函数)或者实例化它们。用这种方式,我们可以用其它的对象来替代他们。比如可以实现同样的接口或者在tests中使用mocks。

但是现在,这些依赖必须要在某些地方被提供,所以依赖注入由提供合作者的类组成。这些通常使用依赖注入器来完成。Dagger可能是Android上最流行的依赖注入器。当然当我们提供依赖有一定复杂性的时候是个很好的替代品。

但是最小的替代是可以在这个构造函数中使用默认值。我们可以给构造函数的参数通过分配默认值的方式提供一个依赖,然后在不同的情况下提供不同的实例。比如,在我们的ForecastDbHelper我们可以用更智能的方式提供一个context:

class ForecastDbHelper(ctx: Context = App.instance) :
        ManagedSQLiteOpenHelper(ctx, ForecastDbHelper.DB_NAME, null,
        ForecastDbHelper.DB_VERSION) {
        ...
}

现在我们有两种方式来创建这个类:

val dbHelper1 = ForecastDbHelper() // 它会使用 App.instance
val dbHelper2 = ForecastDbHelper(mockedContext) // 比如,提供给测试tests

我会到处使用这个特性,所以我在解释清楚之后再继续往下。我们已经有了表,所以是时候开始对它们增加和请求了。但是在这之前,我想先讲讲结合和函数操作符。别忘了查看代码库找到最新的代码。


猜你喜欢

转载自blog.csdn.net/weixin_40119478/article/details/80760327