查看原文
其他

Jetpack新成员AppSearch前来报到

黄林晴 郭霖 2022-12-14


/   今日科技快讯   /

近日,据福建省龙岩市教育局官方微信公众号消息,字节跳动创始人张一鸣向家乡龙岩捐赠5亿元,成立“芳梅教育发展基金”。据“芳梅教育发展基金”相关负责人介绍,该基金主要用途包括辅助全市教师进修、支持职业教育创新发展、改善教育信息化教学环境、设立奖教金以及资助学生宿舍楼等基础设施建设。基金名“芳梅”两个字分别取自张一鸣奶奶和外婆的名字。

/   作者简介   /

本篇文章转载自黄林晴,和大家分享了他对Jetpack的新组件AppSearch的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

黄林晴的博客地址:
https://huanglinqing.blog.csdn.net/

/   前言   /

在今年的Google I/O 大会上,Jetpack库新增了三个组件(刚发布Alpha版本),分别是MarcrobenChmark、AppSearch和Google Shortcuts,MarcrobenChmark组件是用来衡量代码性能一个库,Google Shortcuts听起来像是一种快捷方式,本文我们将着重带领大家领略一下AppSearch的使用。那么什么是AppSearch呢?

/   什么是AppSearch   /

按照官方描述,AppSearch 是一个搜索库,用于管理本地存储的结构化数据,其中包含用于将数据编入索引和通过全文内搜索来检索数据的 API。您可以使用此库来为用户构建自定义的应用内搜索功能。看到应用内搜索,我首先想到了Android设置中的搜索页面,比如我们搜索显示两个字,这里将显示出所有包含“显示”字样的功能入口,如图1所示:

                                          
接下来我们来详细看如何使用AppSearch以及我踩过的那些坑。

/   如何使用   /

引入相关库

首先我们在build.gradle中引入AppSearch组件的相关库,代码如下所示:

def appsearch_version = "1.0.0-alpha01"
implementation("androidx.appsearch:appsearch:$appsearch_version")
kapt("androidx.appsearch:appsearch-compiler:$appsearch_version")
implementation("androidx.appsearch:appsearch-local-storage:$appsearch_version")

在 AppSearch 中,一个数据单元被表示为一个文档。AppSearch 数据库中的每个文档都由其命名空间和 ID 唯一标识。命名空间用于将来自不同来源的数据分开,这一点相当于sql中的表。所以接下来我们来创建一个数据单元。

创建一个数据单元

我们以新闻类为例,创建的数据类如下所示:

@Document
data class News(
    @Document.Namespace
    val namespace: String,
    @Document.Id
    val id: String,
    @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
    val newsTitle: String,
    @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
    val newsContent: String
)

首先在AppSearch中所有的数据单元都要使用@Document注解,namespace和id在上面说了是数据类型的必须字段,newsTitle和newsContent是我们自己定义的新闻标题和新闻内容字段,这里提一下。

@Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)

这个注解,@Document.StringProperty 就是要将字符串类型的变量配置成AppSearch的属性,如果是整型那就是:

@Document.Int64Property

布尔类型就是:

@Document.BooleanProperty

等等等等,indexingType 属性值可以理解为匹配方式,这里设置为INDEXING_TYPE_PREFIXES,如当匹配条件是Huang的时候 可以匹配到HuangLinqing,其他属性感兴趣的可以看下源码androidx.appsearch.app.AppSearchSchema类。创建完数据类之后,同其他数据库操作一样,接下来来创建一个数据库。

创建数据库

创建数据库就会返回给我们一个ListenableFuture,用于整个数据库的操作,代码如下所示:

val context: Context = applicationContext
val sessionFuture = LocalStorage.createSearchSession(
    LocalStorage.SearchContext.Builder(context, /*databaseName=*/"news")
        .build()
)

此时我们可以看到这行代码报了一个错误,错误如下所示:


大致意思是说还需要依赖一个库,说实话,其实AppSearch库完全可以自己依赖一下,这样对开发者方便很多,但是毕竟AppSearch刚出测试版,要求不能太高。

我们在build.gradle中引入guava库,代码如下所示:
 
implementation("com.google.guava:guava:30.1.1-android")

依赖之后,上述代码就可以正常运行了,不过运行的话这里还不行,我们设置java1.8的环境才可以,否则后面运行会出现java.lang.NoSuchMethodError: No static method metafactory的错误。

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
    jvmTarget = "1.8"
}

原问题我提给了Google,可参看
https://issuetracker.google.com/issues/191389033

设置数据模式

AppSearch中有Schema 和schema types的概念,意思是模式和模式类型,模式由表示独特数据类型的模式类型组成这里指的就是News类,模式类型由包含名称、数据类型和基数的属性组成。这里设置数据模式,其实就是指定我们可以在这个名称为“news”的数据空中可以添加什么样类型的数据。

val setChemaRequest = SetSchemaRequest
    .Builder()
    .addDocumentClasses(News::class.java).build()

var setSchemaFuture = Futures.transformAsync(
    sessionFuture,
    AsyncFunction<AppSearchSession?, SetSchemaResponse?> {
        it?.setSchema(setChemaRequest)
    },
    mainExecutor
)

首先我们创建了一个数据类型为News类的模式类,然后通过AppSearchSession的setSchema方法为数据文档设置了数据模式,这里大家疑惑的地方可能是Futures.transformAsync这个方法,其实很简单,Future就是一个Java中的异步线程框架,可以类比为协程,所以说如果AppSearch的设计可以不依赖Future在使用上可能会简单许多。

不过令我差异的是,我咨询了若干做Java的朋友,他们都表示,这个东西很少用。所以这里咱们只专注AppSearch的使用,Futures相关类的使用,感兴趣的可以深入学习下。

设置好数据模式后,我们就可以写入数据了。

写入数据

我们首先定义一个要插入的数据类,如下所示:

val new1 = News(
    namespace = "new1",
    id = "new_id_2",
    newsTitle = "who is a boy",
    newsContent = "Everyone, guess who is the handsome boy"
)

构建PutDocumentsRequest对象并执行。

val putRequest = PutDocumentsRequest.Builder().addDocuments(new1).build()
val putFuture = Futures.transformAsync(
    sessionFuture,
    AsyncFunction<AppSearchSession?, AppSearchBatchResult<String, Void>?> {
        it?.put(putRequest)
    },
    mainExecutor
)

执行结果我们可以通过Futures.addCallback来监听,方法如下所示:

Futures.addCallback(
    putFuture,
    object : FutureCallback<AppSearchBatchResult<String, Void>?> {
        override fun onSuccess(result: AppSearchBatchResult<String, Void>?) {
            // Gets map of successful results from Id to Void
            val successfulResults = result?.successes
            // Gets map of failed results from Id to AppSearchResult
            val failedResults = result?.failures

            Log.d(TAG, "成功:" + successfulResults.toString())
            Log.d(TAG, "失败:" + failedResults.toString())
        }

        override fun onFailure(t: Throwable) {
            Log.d(TAG, t.message.toString())
        }

    },
    mainExecutor
)

运行,程序打印如下所示:

com.lonbon.appsearchdemo D/MainActivity: 成功:{new_id_1=null}
com.lonbon.appsearchdemo D/MainActivity: 失败:{}

说明存储成功了,接下来我们再插入一条数据,插入代码一致,就不重复展示了,数据如下所示:

val news2 = News(
    namespace = "new1",
    id = "new_id_1",
    newsTitle = "Huang Linqing is handsome a boy",
    newsContent = "Huang Linqing is an Android development engineer working in Hefei"
)

查询数据

查询数据首先我们要指定查询的范围要就是namespace,相当于指定数据表,毕竟不同表中可能存在相同符合条件的数据。

val searchSpec = SearchSpec.Builder()
    .addFilterNamespaces("new1")
    .build()

然后执行查询操作,我们这里查询的关键字是“handsome”。

val searchFuture = Futures.transform(
    sessionFuture,
    Function<AppSearchSession?, SearchResults> {
        it?.search("handsome", searchSpec)
    },
    mainExecutor
)

同样的我们使用addCallback方法来检测查询结果,代码如下所示:

Futures.addCallback(
    searchFuture,
    object : FutureCallback<SearchResults> {
        override fun onSuccess(result: SearchResults?) {
            iterateSearchResults(result)
        }

        override fun onFailure(t: Throwable) {
            Log.d(
                TAG, "查询失败:" + t
                    .message
            )
        }

    },
    mainExecutor
)

查询成功会返回SearchResults类,我们需要遍历这个实例取出所有数据打印出来,即iterateSearchResults方法,代码如下所示:

private fun iterateSearchResults(searchResults: SearchResults?) {
    Futures.transform(searchResults?.nextPage, Function<List<SearchResult>, Any> {
        it?.let {
            it.forEach { searchResult ->
                val genericDocument: GenericDocument = searchResult.genericDocument
                val schemaType = genericDocument.schemaType
                if (schemaType == "News") {
                    try {
                        var note = genericDocument.toDocumentClass(News::class.java)

                        Log.d(
                            TAG,
                            "查询结果:新闻标题-" + note.newsTitle
                        )
                        Log.d(
                            TAG,
                            "查询结果:新闻内容-" + note.newsContent
                        )

                    } catch (e: AppSearchException) {
                        Log.e(
                            TAG,
                            "Failed to convert GenericDocument to Note",
                            e
                        )
                    }

                }
            }


        }

    }, mainExecutor)
}

查询出来的结果是一个集合,所以我们需要遍历集合,并且数据类型需要是News类才可以继续下一步,这里我们将符合条件查询的新闻标题打印出来,结果如下所示:

D/MainActivity: 查询结果:新闻标题-who is a boy
.appsearchdemo D/MainActivity: 查询结果:新闻内容-Everyone, guess who is the handsome boy
.appsearchdemo D/MainActivity: 查询结果:新闻标题-Huang Linqing is a handsome boy
.appsearchdemo D/MainActivity: 查询结果:新闻内容-Huang Linqing is an Android development engineer working

这里我们可以看到我们查询的关键字是handsome的时候将两个结果都打印出来了,而第一条结果是新闻标题包含handsome关键字,第二条结果是新闻内容包含关键字,如果我们使用普通的sql,大概需要这么做。

select * from table where newsTitle like %key% or newsContent like %key%

而使用AppSearch 不需要关心具体匹配的是哪个字段,只要任一字段包含相关内容,就将结果显示出来,有点像百度搜索时,我们可以看到有些关键字是在标题中有些关键字是在内容中而这些内容都可以很快的查询出来。

我为什么夸自己

这里我们搜索的关键字是handsome,新闻标题是 Huang Linqing is a handsome boy,黄林晴是个帅气的男孩,这里我并不是故意夸我自己的,而是在学习AppSearch的使用时,我发现了一个bug,那就是上面的代码如果插入的是中文,在搜索时将不会得到任何结果,昨天晚上发现这个问题后我将此问题提给了Google。


Google 也很快给了答复。


不支持中文搜索,这是一个已知问题,并且Google将在新版本中修复,会尽快发布版本,所以我们在新版本发布前知道有这个问题就行了,避免无效检查自己的代码问题。

删除数据

删除数据时我们需指定命名空间和数据id,构建一个删除数据的请求,代码如下所示:

val deleteRequest = RemoveByDocumentIdRequest.Builder("new1")
    .addIds("new_id_2")
    .build()

val removeFuture = Futures.transformAsync(
    sessionFuture, AsyncFunction {
        it?.remove(deleteRequest)
    },
    mainExecutor
)

到这里,我们也看出来了,其实Appsearch的使用,对数据的操作都是先构建一个请求,然后使用Futures去执行,如果需要检测结果的话,就通过Futures.addCallback添加一个回调即可,这里执行删除操作后,我们再次通过关键字“handsome”去查询,会发现就只有一条数据显示出来了,这里执行结果就不在展示了。

关闭会话

在开始使用的时候,我们创建了一个ListenableFuture<AppSearchSession>,后续所有的数据操作都通过这个会话去建立的,在使用结束后我们需要关闭这个会话,代码如下所示:

val closeFuture = Futures.transform<AppSearchSession, Unit>(
    sessionFuture,
    Function {
        it?.close()
    }, mainExecutor
)

/  小结   /

AppSearch是Jetpack最新推出的组件,AppSearch 是一个搜索库,可以很方便的来实现应用内的搜索功能,AppSearch的 I/O 使用很低,与 SQLite 相比,AppSearch 可能会更高效。但目前个人还是认为针对的问题不同和解决问题的角度不同,和其他数据库没有可比性,选择合适的方案最重要。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
我的故事登上了Android开发者的官网
什么?Compose可以开发PC应用了?

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存