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'
}
复制代码
2
3
4
5
而如果你使用的是 Kotlin,那么就在 build.gradle 中引入如下配置:
dependencies {
implementation 'org.litepal.android:kotlin:3.0.0'
}
复制代码
2
3
4
5
好了,接下来我们就一起看一看 LitePal 3.0.0 版本到底变更了哪些东西。
不得不说,其实 LitePal 的泛型设计一直都不是很友好,尤其在异步查询的时候格外难受,比如我们看下如下代码:
在异步查询的onFinish()
回调中,我们直接得到的并不是查询的对象,而是一个泛型 T 对象,还需要再经过一次强制转型才能得到真正想要查询的对象。
如果你觉得这还不算难受的话,那么再来看看下面这个例子:
可以看到,这次查询返回的是一个List<T>
,我们必须要对整个 List 进行强制转型。不仅要多写一行代码,关键是开发工具还会给出一个很丑的警告。
这样的设计无论如何都算不上友好。
这里非常感谢 xiazunyang 这位朋友在 GitHub 上提出的这个 Issue(github.com/LitePalFram… (opens new window)%EF%BC%8C%E5%B9%B6%E4%B8%94%E7%BB%99%E5%87%BA%E4%BA%86%E5%BB%BA%E8%AE%AE%E7%9A%84%E4%BC%98%E5%8C%96%E6%96%B9%E6%A1%88%EF%BC%8CLitePal) 3.0.0 版本在泛型方面的优化很大程度上是基于他的建议。
那么我们现在来看看,到了 LitePal 3.0.0 版本,同样的功能可以怎么写:
LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
@Override
public void onFinish(Song song) {
}
});
复制代码
2
3
4
5
6
7
8
可以看到,这里在FindCallback
接口上声明了泛型类型为Song
,那么在onFinish()
方法回调中的参数就可以直接指定为Song
类型了,从而避免了一次强制类型转换。
那么同样地,在查询多条数据的时候就可以这样写:
LitePal.where("duration > ?", "100").findAsync(Song.class).listen(new FindMultiCallback<Song>() {
@Override
public void onFinish(List<Song> list) {
}
});
复制代码
2
3
4
5
6
7
8
这次就清爽多了吧,在onFinish()
回调方法中,我们直接拿到的就是一个List<Song>
集合,而不会再出现那个丑丑的警告了。
而如果这段代码使用 Kotlin 来编写的话,将会更加的精简:
LitePal.where("duration > ?", "100").findAsync(Song::class.java).listen { list ->
}
复制代码
2
3
4
5
得益于 Kotlin 出色的 lambda 机制,我们的代码可以得到进一步精简。在上述代码中,行尾的list
参数就是查询出来的List<Song>
集合了。
那么关于泛型优化的讲解就到这里,下面我们来看另一个主题,监听数据库的创建和升级。
没错,LitePal 3.0.0 版本新增了监听数据库的创建和升级功能。
加入这个功能是因为 JakeHao 这位朋友在 GitHub 上提了一个 Issue(github.com/LitePalFram… (opens new window)%EF%BC%8C%E5%9C%A8%E4%BB%96%E8%AF%B4%E6%98%8E%E4%BA%86%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%E4%B9%8B%E5%90%8E%EF%BC%8C%E6%88%91%E8%AE%A4%E4%B8%BA%E7%9B%91%E5%90%AC%E6%95%B0%E6%8D%AE%E5%BA%93%E5%88%9B%E5%BB%BA%E5%92%8C%E5%8D%87%E7%BA%A7%E8%BF%99%E4%B8%AA%E5%8A%9F%E8%83%BD%E8%BF%98%E6%98%AF%E9%9D%9E%E5%B8%B8%E6%9C%89%E6%84%8F%E4%B9%89%E7%9A%84%E3%80%82)
)
要实现这个功能肯定要添加新的接口了,而我对于添加新接口保持着一种比较谨慎的态度,因为要考虑到接口的易用性和对整体框架的影响。
LitePal 的每一个接口我都要尽量将它设计得简单好用,因此大家应该也可以猜到了,监听数据库创建和升级这个功能会非常容易,只需要简单几行代码就可以了实现了:
LitePal.registerDatabaseListener(new DatabaseListener() {
@Override
public void onCreate() {
}
@Override
public void onUpgrade(int oldVersion, int newVersion) {
}
});
复制代码
2
3
4
5
6
7
8
9
10
11
需要注意的是,registerDatabaseListener()
方法一定要确保在任何其他数据库操作之前调用,然后当数据库创建的时候,onCreate()
方法就会得到回调,当数据库升级的时候onUpgrade()
方法就会得到回调,并且告诉通过参数告诉你之前的老版本号,以及升级之后的新版本号。
Kotlin 版的代码也是类似的,但是由于这个接口有两个回调方法,因此用不了 Kotlin 的单抽象方法 (SAM) 这种语法糖,只能使用实现接口的匿名对象这种写法:
LitePal.registerDatabaseListener(object : DatabaseListener {
override fun onCreate() {
}
override fun onUpgrade(oldVersion: Int, newVersion: Int) {
}
})
复制代码
2
3
4
5
6
7
8
9
这样我们就将监听数据库创建和升级这部分内容也快速介绍完了,接下来即将进入到本篇文章的重头戏内容。
从上述文章中我们都可以看出,Kotlin 版的代码普遍都是比 Java 代码要更简约的,Google 给出的官方统计是,使用 Kotlin 开发可以减少大约 25% 以上的代码。
但是处处讲究简约的 Kotlin,却在有一处用法上让我着实很难受。比如使用 Java 查询 song 表中 id 为 1 的这条记录是这样写的:
Song song = LitePal.find(Song.class, 1);
复制代码
2
3
而同样的功能在 Kotlin 中却需要这样写:
val song = LitePal.find(Song::class.java, 1)
复制代码
2
3
由于 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) {
}
复制代码
2
3
4
5
上述方法中的泛型 T 就是一个被实化的泛型,因为它满足了内联函数和reified
关键字这两个前提条件。那么借助泛型实化,我们到底可以实现什么样的效果呢?从方法名上就可以看出来了,这里我们借助泛型来实现一个 instanceOf 的效果,代码如下所示:
inline fun <reified T> instanceOf(value: Any) = value is T
复制代码
2
3
虽然只有一行代码,但是这里实现了一个 Java 中完全不可能实现的功能 —— 判断参数的类型是不是属于泛型的类型。这就是泛型实化不可思议的地方。
那么我们如何使用这个方法呢?在 Kotlin 中可以这么写:
val result1 = instanceOf<String>("hello")
val result2 = instanceOf<String>(123)
复制代码
2
3
4
5
可以看到,第一行代码指定的泛型是String
,参数是字符串"hello"
,因此最后的结果是true
。而第二行代码指定泛型是String
,参数却是数字123
,因此最后的结果是false
。
除了可以做类型判断之外,我们还可以直接获取到泛型的 Class 类型。看一下下面的代码:
inline fun <reified T> genericClass() = T::class.java
复制代码
2
3
这段代码就更加不可思议了,genericClass()
方法直接返回了当前指定泛型的 class 类型。T.class
这样的语法在 Java 中是不可能的,而在 Kotlin 中借助泛型实化功能就可以使用T::class.java
这样的语法了。
然后我们就可以这样调用:
val result1 = genericClass<String>()
val result2 = genericClass<Int>()
复制代码
2
3
4
5
可以看到,我们如果指定了泛型String
,那么最终就可以得到java.lang.String
的 Class,如果指定了泛型Int
,最终就可以得到java.lang.Integer
的 Class。
关于 Kotlin 泛型实化这部分的讲解就到这里,现在我们重新回到 LitePal 上面。讲了这么多泛型实化方面的内容,那么 LitePal 到底如何才能利用这个特性进行优化呢?
回顾一下,刚才我们查询 song 表中 id 为 1 的这条记录是这样写的:
val song = LitePal.find(Song::class.java, 1)
复制代码
2
3
这里需要传入Song::class.java
是因为要告知 LitePal 去查询 song 这张表中的数据。而通过刚才泛型实化部分的讲解,我们知道 Kotlin 中是可以使用T::class.java
这样的语法的,因此我在 LitePal 3.0.0 中扩展了这部分特性,允许通过指定泛型来声明查询哪张表中的内容。于是代码就可以优化成这个样子了:
val song = LitePal.find<Song>(1)
复制代码
2
3
怎么样,有没有觉得代码瞬间清爽了很多?看起来比 Java 版的查询还要更加简约。
另外得益于 Kotlin 出色的类型推导机制,我们还可以将代码改为如下写法:
val song: Song? = LitePal.find(1)
复制代码
2
3
这两种写法效果是一模一样的,因为如果我在song
变量的后面声明了Song?
类型,那么find()
方法就可以自动推导出泛型类型,从而不需要再手动进行<Song>
的泛型指定了。
除了find()
方法之外,我还对 LitePal 中几乎全部的公有 API 都进行了优化,只要是原来需要传递 Class 参数的接口,我都增加了一个通过指定泛型来替代 Class 参数的扩展方法。注意,这里我使用的是扩展方法,而不是修改了原有方法,这样的话两种写法你都可以使用,全凭自己的喜好,如果是直接修改原有方法,那么项目升级之后就可能会造成大面积报错了,这是谁都不想看到的。
那么这里我再向大家演示另外几种 CRUD 操作优化之后的用法吧,比如我想使用 where 条件查询的时候就可以这样写:
val list = LitePal.where("duration > ?", "100").find<Song>()
复制代码
2
3
这里在最后的 find() 方法中指定了泛型<Song>
,得到的结果会是一个List<Song>
集合。
想要删除 song 表中 id 为 1 的这条数据可以这么写:
LitePal.delete<Song>(1)
复制代码
2
3
想要统计 song 表中的记录数量可以这么写:
val count = LitePal.count<Song>()
复制代码
2
3
其他一些方法的优化也都是类似的,相信大家完全可以举一反三,就不再一一演示了。
这样我们就将 LitePal 新版本中的主要功能都介绍完了。当然,除了这些新功能之外,我还修复了一些已知的 bug,提升了整体框架的稳定性,如果这些正是你所需要的话,那就赶快升级吧。
作者:郭霖 链接:https://juejin.cn/post/6997612373032828965 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。