Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 9, 2021 09:37 am GMT

Hands on Jetpack AppSearch

Among many others, Android 12 has one particularly cool new feature: AppSearch. It allows you to store information about your app data in a search engine and retrieve it later using full text search. As the search happens locally on the device, users can find information even when the actual data is in the cloud.

To make this feature available for older platforms, Google has created a new Jetpack component called Jetpack AppSearch. It's currently in alpha, so expect changes to the apis. This hands on article shows you how to use the library. As new versions are released, I plan update both this article and the accompanying code. The sample app is on GitHub.

Let's start by declaring dependencies.

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'}

As you will see shortly, Jetpack AppSearch heavily relies on ListenableFuture. It seems that for now you need to include Guava to get it. Tis may change in the future, though. Also, you will need to work with quite a few annotations. I suggest you use Kotlin annotation processing, as you can see in the line starting with kapt. This implies that you need to activate the corresponding plugin:

plugins {  id 'com.android.application'  id 'kotlin-android'  id "kotlin-kapt"}

One final note regarding build.gradle. Have you noticed that I use androidx.lifecycle?. You need to setup and tear down AppSearch, and I think this is best decoupled from the activity using lifecycle.

Documents

The information to be stored and retrieved is modelled as documents. A simple document description looks like this:

@Documentdata 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)

The namespace is an arbitrary user-provided string. It is used to group documents during querying or deletion. Indexing a document with a particular id replaces any existing documents with the same id in that namespace. The id is the document's unique identifier. A document must have exactly one such field. The score is an indication of the document's quality, relative to other documents of the same type. It can be used in queries. The field is optional. If it's not provided, the document will have a score of 0.

@Document.StringProperty makes message known to AppSearch. AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES means that the content in this property should be returned for queries that are exact matches or query matches of the tokens appearing in this property.

Next, let's see how to setup and tear down AppSearch

Setting up AppSearch

AppSearch must be setup prior to being used. And if you no longer need it, you should clean a few things up. I found it most convenient to tie this to 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    )  }}

So, I have implemented a LifecycleObserver that reacts to Lifecycle.Event.ON_RESUME and Lifecycle.Event.ON_PAUSE. The main access point to the rest of the app is sessionFuture. On older platforms, AppSearch uses a search engine local to the app, whereas on Android 12 it can rely on a system-wide version. This distinction is made in 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()    }  }

Now we can actually use AppSearch.

Using AppSearch

Inside the main activity of my sample app I (in onCreate()) I create an instance of AppSearchObserver and pass it to lifecycle.addObserver(). The actual work is done in a coroutine, which is started like this: lifecycleScope.launchWhenResumed { ....

First we set up a schema:

private fun setSchema() {  val setSchemaRequest =    SetSchemaRequest.Builder().addDocumentClasses(MyDocument::class.java)      .build()  /* val setSchemaFuture = */ Futures.transformAsync(    appSearchObserver.sessionFuture,    { session ->      session?.setSchema(setSchemaRequest)    }, mainExecutor  )}

The current version of the library relies on ListenableFutures, which certainly are a modern programming paradigm. On the other hand, Kotlin Flows are used in so many other places. This makes me wonder why the team decided to not use them. It appears to be a planned feature for some time in the future, though.

Adding a document looks like this:

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  )}

So, in essence you create an instance of your document and pass it to AppSearch by creating a put request with PutDocumentsRequest.Builder().addDocuments(doc).build().

Next, let's look at an example of performing a search:

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  )}

So, we first need a search specification: SearchSpec.Builder() ... .build(). Then we invoke the search using our sessionFuture. As you can see, the actual retrieval takes place inside iterateSearchResults(). The idea of the api is to iterate over pages using searchResults.nextPage. My example uses only the first page. That's why I configured the search using .setResultCountPerPage(100). I assume this is not a best practice , but for a demo it should do.

The last function we will look at is persist. As the name suggests, you need to persist change you make to the database.

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)}

So, the mechanics are:

  • obtain a ListenableFuture using Futures.transform()
  • if needed, add a callback using Futures.addCallback()

Conclusion

Frankly, I am still in the process of familiarizing myself with the library. I find the code very verbose and not easy to understand. What's your impression? Am I missing the point? Please share your thoughts in the comments.


Original Link: https://dev.to/tkuenneth/hands-on-jetpack-appsearch-2cjc

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To