其中,Android 12 有一项特别酷的新功能:AppSearch。它允许您将有关应用程序数据的信息存储在搜索引擎中,并在以后使用全文搜索检索它。由于搜索在设备本地进行,即使实际数据在云端,用户也可以找到信息。
为了使该功能可用于旧平台,Google 创建了一个名为Jetpack AppSearch的新 Jetpack 组件。它目前处于 alpha 阶段,因此期待对 api 的更改。这篇动手文章向您展示了如何使用该库。随着新版本的发布,我计划更新本文和随附的代码。示例应用程序位于GitHub 上。
让我们从声明依赖项开始。
dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
def appsearch_version = "1.0.0-alpha03"
implementation "androidx.appsearch:appsearch:$appsearch_version"
kapt "androidx.appsearch:appsearch-compiler:$appsearch_version"
implementation "androidx.appsearch:appsearch-local-storage:$appsearch_version"
implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version"
// See similar issue: https://stackoverflow.com/a/64733418
implementation 'com.google.guava:guava:30.1.1-android'
}
您很快就会看到,Jetpack AppSearch 严重依赖ListenableFuture
. 看来现在你需要包括 Guava 才能得到它。不过,这可能会在未来发生变化。此外,您将需要使用相当多的注释。我建议您使用 Kotlin 注释处理,正如您在以kapt
. 这意味着您需要激活相应的插件:
plugins {
id 'com.android.application'
id 'kotlin-android'
id "kotlin-kapt"
}
最后一点关于build.gradle。你注意到我用了androidx.lifecycle
吗?您需要设置和拆除 AppSearch,我认为这最好与使用生命周期的活动分离。
文件
要存储和检索的信息被建模为文档。一个简单的文档描述如下所示:
@Document
data class MyDocument(
@Document.Namespace
val namespace: String,
@Document.Id
val id: String,
@Document.Score
val score: Int,
@Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
val message: String
)
是用户提供的namespace
任意字符串。它用于在查询或删除期间对文档进行分组。使用特定索引文档id
会替换该命名空间中具有相同内容的任何现有文档id
。id
是文档的唯一标识符。一个文档必须恰好有一个这样的字段。score
相对于相同类型的其他文档,这是文档质量的指示。它可以在查询中使用。该字段是可选的。如果未提供,则该文档的得分为 0。
@Document.StringProperty
让message
AppSearch 知道。AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES
表示对于完全匹配的查询或出现在此属性中的标记的查询匹配,应返回此属性中的内容。
接下来,让我们看看如何设置和拆除 AppSearch
设置应用搜索
AppSearch 必须在使用前进行设置。如果你不再需要它,你应该清理一些东西。我发现将它与以下内容联系起来最方便lifecycle
:
private const val TAG = "AppSearchDemoActivity"
private const val DATABASE_NAME = "appsearchdemo"
class AppSearchObserver(private val context: Context) : LifecycleObserver {
lateinit var sessionFuture: ListenableFuture<AppSearchSession>
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun setupAppSearch() {
sessionFuture = if (BuildCompat.isAtLeastS()) {
PlatformStorage.createSearchSession(
PlatformStorage.SearchContext.Builder(context, DATABASE_NAME)
.build()
)
} else {
LocalStorage.createSearchSession(
LocalStorage.SearchContext.Builder(context, DATABASE_NAME)
.build()
)
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun teardownAppSearch() {
/* val closeFuture = */ Futures.transform<AppSearchSession, Unit>(
sessionFuture,
{ session ->
session?.close()
Unit
}, context.mainExecutor
)
}
}
所以,我已经实现了一个对andLifecycleObserver
做出反应的。应用程序其余部分的主要访问点是. 在旧平台上,AppSearch 使用应用程序本地的搜索引擎,而在 Android 12 上,它可以依赖于系统范围的版本。这种区别是在.Lifecycle.Event.ON_RESUME``Lifecycle.Event.ON_PAUSE``sessionFuture``setupAppSearch()
class AppSearchDemoActivity : AppCompatActivity() {
private lateinit var appSearchObserver: AppSearchObserver
private lateinit var binding: MainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainBinding.inflate(layoutInflater)
setContentView(binding.root)
appSearchObserver = AppSearchObserver(applicationContext)
lifecycle.addObserver(appSearchObserver)
lifecycleScope.launchWhenResumed {
setSchema()
addDocument()
search()
persist()
}
}
现在我们可以实际使用 AppSearch。
使用应用搜索
在我的示例应用程序的主要活动中,我(在 中onCreate()
)创建了一个实例AppSearchObserver
并将其传递给lifecycle.addObserver()
. 实际的工作是在一个协程中完成的,它是这样开始的:lifecycleScope.launchWhenResumed { ...
.
首先我们建立一个模式:
private fun setSchema() {
val setSchemaRequest =
SetSchemaRequest.Builder().addDocumentClasses(MyDocument::class.java)
.build()
/* val setSchemaFuture = */ Futures.transformAsync(
appSearchObserver.sessionFuture,
{ session ->
session?.setSchema(setSchemaRequest)
}, mainExecutor
)
}
当前版本的库依赖于ListenableFuture
s,这无疑是一种现代编程范式。另一方面,KotlinFlow
被用在很多其他地方。这让我想知道为什么团队决定不使用它们。不过,这似乎是未来一段时间的计划功能。
添加文档如下所示:
private fun addDocument() {
val doc = MyDocument(
namespace = packageName,
id = UUID.randomUUID().toString(),
score = 10,
message = "Hello, this doc was created ${Date()}"
)
val putRequest = PutDocumentsRequest.Builder().addDocuments(doc).build()
val putFuture = Futures.transformAsync(
appSearchObserver.sessionFuture,
{ session ->
session?.put(putRequest)
}, mainExecutor
)
Futures.addCallback(
putFuture,
object : FutureCallback<AppSearchBatchResult<String, Void>?> {
override fun onSuccess(result: AppSearchBatchResult<String, Void>?) {
output("successfulResults = ${result?.successes}")
output("failedResults = ${result?.failures}")
}
override fun onFailure(t: Throwable) {
output("Failed to put document(s).")
Log.e(TAG, "Failed to put document(s).", t)
}
},
mainExecutor
)
}
因此,本质上,您创建了一个文档实例,并通过创建一个 put 请求将其传递给 AppSearch PutDocumentsRequest.Builder().addDocuments(doc).build()
。
接下来,让我们看一个执行搜索的示例:
private fun search() {
val searchSpec = SearchSpec.Builder()
.addFilterNamespaces(packageName)
.setResultCountPerPage(100)
.build()
val searchFuture = Futures.transform(
appSearchObserver.sessionFuture,
{ session ->
session?.search("hello", searchSpec)
},
mainExecutor
)
Futures.addCallback(
searchFuture,
object : FutureCallback<SearchResults> {
override fun onSuccess(searchResults: SearchResults?) {
searchResults?.let {
iterateSearchResults(searchResults)
}
}
override fun onFailure(t: Throwable?) {
Log.e("TAG", "Failed to search in AppSearch.", t)
}
},
mainExecutor
)
}
private fun iterateSearchResults(searchResults: SearchResults) {
Futures.transform(
searchResults.nextPage,
{ page: List<SearchResult>? ->
page?.forEach { current ->
val genericDocument: GenericDocument = current.genericDocument
val schemaType = genericDocument.schemaType
val document: MyDocument? = try {
if (schemaType == "MyDocument") {
genericDocument.toDocumentClass(MyDocument::class.java)
} else null
} catch (e: AppSearchException) {
Log.e(
TAG,
"Failed to convert GenericDocument to MyDocument",
e
)
null
}
output("Found ${document?.message}")
}
},
mainExecutor
)
}
所以,我们首先需要一个搜索规范:SearchSpec.Builder() ... .build()
. 然后我们使用我们的sessionFuture
. 如您所见,实际的检索发生在iterateSearchResults()
. api 的想法是使用searchResults.nextPage
. 我的示例仅使用第一页。这就是为什么我使用.setResultCountPerPage(100)
. 我认为这不是最佳实践😂,但对于演示它应该这样做。
我们要看的最后一个函数是persist
. 顾名思义,您需要持久化对数据库所做的更改。
private fun persist() {
val requestFlushFuture = Futures.transformAsync(
appSearchObserver.sessionFuture,
{ session -> session?.requestFlush() }, mainExecutor
)
Futures.addCallback(requestFlushFuture, object : FutureCallback<Void?> {
override fun onSuccess(result: Void?) {
// Success! Database updates have been persisted to disk.
}
override fun onFailure(t: Throwable) {
Log.e(TAG, "Failed to flush database updates.", t)
}
}, mainExecutor)
}
所以,机制是:
- 获得
ListenableFuture
使用Futures.transform()
- 如果需要,使用添加回调
Futures.addCallback()
结论
坦率地说,我还在熟悉图书馆的过程中。我发现代码非常冗长且不易理解。你的印象是什么?我错过了重点吗?请在评论中分享您的想法。
网友评论