美文网首页
实践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