美文网首页
实践Jetpack AppSearch

实践Jetpack AppSearch

作者: 安安_660c | 来源:发表于2022-07-30 10:12 被阅读0次

其中,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会替换该命名空间中具有相同内容的任何现有文档idid是文档的唯一标识符。一个文档必须恰好有一个这样的字段。score相对于相同类型的其他文档,这是文档质量的指示。它可以在查询中使用。该字段是可选的。如果未提供,则该文档的得分为 0。

@Document.StringPropertymessageAppSearch 知道。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
  )
}

当前版本的库依赖于ListenableFutures,这无疑是一种现代编程范式。另一方面,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()

结论

坦率地说,我还在熟悉图书馆的过程中。我发现代码非常冗长且不易理解。你的印象是什么?我错过了重点吗?请在评论中分享您的想法。

链接:https://dev.to/tkuenneth/hands-on-jetpack-appsearch-2cjc

相关文章

网友评论

      本文标题:实践Jetpack AppSearch

      本文链接:https://www.haomeiwen.com/subject/bamcwrtx.html