Android数据库高手秘籍,如何在Kotlin中更好地使用LitePal
前言
自从 LitePal 在 2.0.0 版本中全面支持了 Kotlin 之后,我也一直在思考如何让 LitePal 更好地融入和适配 Kotlin 语言,而不仅仅停留在简单的支持层面。
Kotlin 确实是一门非常出色的语言,里面有许多优秀的特性是在 Java 中无法实现的。因此,在 LitePal 全面支持了 Kotlin 之后,我觉得如果我还视这些优秀特性而不见的话,就有些太暴殄天物了。所以在最新的 LitePal 3.0.0 版本里面,我准备让 LitePal 更加充分地利用 Kotlin 的一些语言特性,从而让我们的开发更加轻松。
本篇文章除了介绍 LitePal 3.0.0 版本的升级内容之外,还会讲解一些 Kotlin 方面的高级知识。
首先还是来看如何升级。
升级的方式
为什么这次的版本号跨度如此之大,直接从 2.0 升到了 3.0 呢?因为这次 LitePal 在结构上面有了一个质的变化。
为了更好地兼容 Kotlin 语言,LitePal 现在不再只是一个库了,而是变成了两个库,根据你使用的语言不同,需要引入的库也不同。如果你使用的是 Java,那么就在 build.gradle 中引入如下配置:
dependencies {
implementation 'org.litepal.android:java:3.0.0'
}
而如果你使用的是 Kotlin,那么就在 build.gradle 中引入如下配置:
dependencies {
implementation 'org.litepal.android:kotlin:3.0.0'
}
好了,接下来我们就一起看一看 LitePal 3.0.0 版本到底变更了哪些东西。
不得不说,其实 LitePal 的泛型设计一直都不是很友好,尤其在异步查询的时候格外难受,比如我们看下如下代码:
在异步查询的onFinish()
回调中,我们直接得到的并不是查询的对象,而是一个泛型 T 对象,还需要再经过一次强制转型才能得到真正想要查询的对象。
如果你觉得这还不算难受的话,那么再来看看下面这个例子:
可以看到,这次查询返回的是一个List<T>
,我们必须要对整个 List 进行强制转型。不仅要多写一行代码,关键是开发工具还会给出一个很丑的警告。
这样的设计无论如何都算不上友好。
这里非常感谢 xiazunyang 这位朋友在 GitHub 上提出的这个 Issue(github.com/LitePalFram… 3.0.0 版本在泛型方面的优化很大程度上是基于他的建议。
那么我们现在来看看,到了 LitePal 3.0.0 版本,同样的功能可以怎么写:
LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
@Override
public void onFinish(Song song) {
}
});
可以看到,这里在FindCallback
接口上声明了泛型类型为Song
,那么在onFinish()
方法回调中的参数就可以直接指定为Song
类型了,从而避免了一次强制类型转换。
那么同样地,在查询多条数据的时候就可以这样写:
LitePal.where("duration > ?", "100").findAsync(Song.class).listen(new FindMultiCallback<Song>() {
@Override
public void onFinish(List<Song> list) {
}
});
这次就清爽多了吧,在onFinish()
回调方法中,我们直接拿到的就是一个List<Song>
集合,而不会再出现那个丑丑的警告了。
而如果这段代码使用 Kotlin 来编写的话,将会更加的精简:
LitePal.where("duration > ?", "100").findAsync(Song::class.java).listen { list ->
}
得益于 Kotlin 出色的 lambda 机制,我们的代码可以得到进一步精简。在上述代码中,行尾的list
参数就是查询出来的List<Song>
集合了。
那么关于泛型优化的讲解就到这里,下面我们来看另一个主题,监听数据库的创建和升级。
没错,LitePal 3.0.0 版本新增了监听数据库的创建和升级功能。
加入这个功能是因为 JakeHao 这位朋友在 GitHub 上提了一个 Issue(github.com/LitePalFram…
)
要实现这个功能肯定要添加新的接口了,而我对于添加新接口保持着一种比较谨慎的态度,因为要考虑到接口的易用性和对整体框架的影响。
LitePal 的每一个接口我都要尽量将它设计得简单好用,因此大家应该也可以猜到了,监听数据库创建和升级这个功能会非常容易,只需要简单几行代码就可以了实现了:
LitePal.registerDatabaseListener(new DatabaseListener() {
@Override
public void onCreate() {
}
@Override
public void onUpgrade(int oldVersion, int newVersion) {
}
});
需要注意的是,registerDatabaseListener()
方法一定要确保在任何其他数据库操作之前调用,然后当数据库创建的时候,onCreate()
方法就会得到回调,当数据库升级的时候onUpgrade()
方法就会得到回调,并且告诉通过参数告诉你之前的老版本号,以及升级之后的新版本号。
Kotlin 版的代码也是类似的,但是由于这个接口有两个回调方法,因此用不了 Kotlin 的单抽象方法 (SAM) 这种语法糖,只能使用实现接口的匿名对象这种写法:
LitePal.registerDatabaseListener(object : DatabaseListener {
override fun onCreate() {
}
override fun onUpgrade(oldVersion: Int, newVersion: Int) {
}
})
这样我们就将监听数据库创建和升级这部分内容也快速介绍完了,接下来即将进入到本篇文章的重头戏内容。
从上述文章中我们都可以看出,Kotlin 版的代码普遍都是比 Java 代码要更简约的,Google 给出的官方统计是,使用 Kotlin 开发可以减少大约 25% 以上的代码。
但是处处讲究简约的 Kotlin,却在有一处用法上让我着实很难受。比如使用 Java 查询 song 表中 id 为 1 的这条记录是这样写的:
Song song = LitePal.find(Song.class, 1);
而同样的功能在 Kotlin 中却需要这样写:
val song = LitePal.find(Song::class.java, 1)
由于 LitePal 必须知道要查询哪个表当中的数据,因此一定要传递一个 Class 参数给 LitePal 才行。在 Java 中我们只需要传入Song.class
即可,但是在 Kotlin 中的写法却变成了Song::class.java
,反而比 Java 代码更长了,有没有觉得很难受?
当然,很多人写着写着也就习惯了,这并不是什么大问题。但是随着我深入学习 Kotlin 之后,我发现 Kotlin 提供了一个相当强大的机制可以优化这个问题,这个机制叫作泛型实化。接下来我会对泛型实化的概念和用法做个详细的讲解。
要理解泛型实化,首先你需要知道泛型擦除的概念。
不管是 Java 还是 Kotlin,只要是基于 JVM 的语言,泛型基本都是通过类型擦除来实现的。也就是说泛型对于类型的约束只在编译时期存在,运行时期是无法直接对泛型的类型进行检查的。例如,我们创建一个List<String>
集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期 JVM 却并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List
。
Java 的泛型擦除机制,使得我们不可能使用if (a instanceof T)
,或者是T.class
这样的语法。
而 Kotlin 也是基于 JVM 的语言,因此 Kotlin 的泛型在运行时也是会被擦除的。但是 Kotlin 中提供了一个内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这就使得原有方法调用时的形参声明和实参传递,在编译之后直接变成了同一个方法内的变量调用。这样的话也就不存在什么泛型擦除的问题了,因为 Kotlin 在编译之后会直接使用实参替代内联方法中泛型部分的代码。
简单点来说,就是 Kotlin 是允许将内联方法中的泛型进行实化的。
泛型实化
那么具体该怎么写才能将泛型实化呢?首先,该方法必须是内联方法才行,也就是要用inline
关键字来修饰该方法。其次,在声明泛型的地方还必须加上reified
关键字来表示该泛型要进行实化。示例代码如下所示:
inline fun <reified T> instanceOf(value: Any) {
}
上述方法中的泛型 T 就是一个被实化的泛型,因为它满足了内联函数和reified
关键字这两个前提条件。那么借助泛型实化,我们到底可以实现什么样的效果呢?从方法名上就可以看出来了,这里我们借助泛型来实现一个 instanceOf 的效果,代码如下所示:
inline fun <reified T> instanceOf(value: Any) = value is T
虽然只有一行代码,但是这里实现了一个 Java 中完全不可能实现的功能 —— 判断参数的类型是不是属于泛型的类型。这就是泛型实化不可思议的地方。
那么我们如何使用这个方法呢?在 Kotlin 中可以这么写:
val result1 = instanceOf<String>("hello")
val result2 = instanceOf<String>(123)
可以看到,第一行代码指定的泛型是String
,参数是字符串"hello"
,因此最后的结果是true
。而第二行代码指定泛型是String
,参数却是数字123
,因此最后的结果是false
。
除了可以做类型判断之外,我们还可以直接获取到泛型的 Class 类型。看一下下面的代码:
inline fun <reified T> genericClass() = T::class.java
这段代码就更加不可思议了,genericClass()
方法直接返回了当前指定泛型的 class 类型。T.class
这样的语法在 Java 中是不可能的,而在 Kotlin 中借助泛型实化功能就可以使用T::class.java
这样的语法了。
然后我们就可以这样调用:
val result1 = genericClass<String>()
val result2 = genericClass<Int>()
可以看到,我们如果指定了泛型String
,那么最终就可以得到java.lang.String
的 Class,如果指定了泛型Int
,最终就可以得到java.lang.Integer
的 Class。
关于 Kotlin 泛型实化这部分的讲解就到这里,现在我们重新回到 LitePal 上面。讲了这么多泛型实化方面的内容,那么 LitePal 到底如何才能利用这个特性进行优化呢?
回顾一下,刚才我们查询 song 表中 id 为 1 的这条记录是这样写的:
val song = LitePal.find(Song::class.java, 1)
这里需要传入Song::class.java
是因为要告知 LitePal 去查询 song 这张表中的数据。而通过刚才泛型实化部分的讲解,我们知道 Kotlin 中是可以使用T::class.java
这样的语法的,因此我在 LitePal 3.0.0 中扩展了这部分特性,允许通过指定泛型来声明查询哪张表中的内容。于是代码就可以优化成这个样子了:
val song = LitePal.find<Song>(1)
怎么样,有没有觉得代码瞬间清爽了很多?看起来比 Java 版的查询还要更加简约。
另外得益于 Kotlin 出色的类型推导机制,我们还可以将代码改为如下写法:
val song: Song? = LitePal.find(1)
这两种写法效果是一模一样的,因为如果我在song
变量的后面声明了Song?
类型,那么find()
方法就可以自动推导出泛型类型,从而不需要再手动进行<Song>
的泛型指定了。
除了find()
方法之外,我还对 LitePal 中几乎全部的公有 API 都进行了优化,只要是原来需要传递 Class 参数的接口,我都增加了一个通过指定泛型来替代 Class 参数的扩展方法。注意,这里我使用的是扩展方法,而不是修改了原有方法,这样的话两种写法你都可以使用,全凭自己的喜好,如果是直接修改原有方法,那么项目升级之后就可能会造成大面积报错了,这是谁都不想看到的。
那么这里我再向大家演示另外几种 CRUD 操作优化之后的用法吧,比如我想使用 where 条件查询的时候就可以这样写:
val list = LitePal.where("duration > ?", "100").find<Song>()
这里在最后的 find() 方法中指定了泛型<Song>
,得到的结果会是一个List<Song>
集合。
想要删除 song 表中 id 为 1 的这条数据可以这么写:
LitePal.delete<Song>(1)
想要统计 song 表中的记录数量可以这么写:
val count = LitePal.count<Song>()
其他一些方法的优化也都是类似的,相信大家完全可以举一反三,就不再一一演示了。
这样我们就将 LitePal 新版本中的主要功能都介绍完了。当然,除了这些新功能之外,我还修复了一些已知的 bug,提升了整体框架的稳定性,如果这些正是你所需要的话,那就赶快升级吧。