From 9dc30d1c49295d97c11871317a97be8101372087 Mon Sep 17 00:00:00 2001 From: Mouaad Aallam Date: Tue, 21 Feb 2023 15:45:43 +0100 Subject: [PATCH] chore(examples): add query suggestion guide --- examples/android/src/main/AndroidManifest.xml | 6 +- .../examples/android/directory/Directory.kt | 2 + .../android/guides/querysuggestion/Product.kt | 30 ++++++++++ .../guides/querysuggestion/ProductAdapter.kt | 41 +++++++++++++ .../guides/querysuggestion/ProductFragment.kt | 38 ++++++++++++ .../QuerySuggestionActivity.kt | 60 +++++++++++++++++++ .../QuerySuggestionViewModel.kt | 41 +++++++++++++ .../querysuggestion/SuggestionAdapter.kt | 45 ++++++++++++++ .../querysuggestion/SuggestionFragment.kt | 40 +++++++++++++ 9 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/Product.kt create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductAdapter.kt create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductFragment.kt create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionActivity.kt create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionViewModel.kt create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionAdapter.kt create mode 100644 examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionFragment.kt diff --git a/examples/android/src/main/AndroidManifest.xml b/examples/android/src/main/AndroidManifest.xml index 77736ee1d..c07026502 100644 --- a/examples/android/src/main/AndroidManifest.xml +++ b/examples/android/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ + + , + val price: Price, + val description: String, + override val objectID: ObjectID, + override val _highlightResult: JsonObject? +) : Indexable, Highlightable { + + val highlightedName: HighlightedString? + get() = getHighlight(Attribute("name")) +} + +@Serializable +data class Price( + val currency: String, + val value: String, +) diff --git a/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductAdapter.kt b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductAdapter.kt new file mode 100644 index 000000000..4c9160371 --- /dev/null +++ b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductAdapter.kt @@ -0,0 +1,41 @@ +package com.algolia.instantsearch.examples.android.guides.querysuggestion + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.algolia.instantsearch.android.highlighting.toSpannedString +import com.algolia.instantsearch.android.inflate +import com.algolia.instantsearch.core.hits.HitsView +import com.algolia.instantsearch.examples.android.R +import com.algolia.instantsearch.examples.android.guides.querysuggestion.ProductAdapter.ProductViewHolder + +class ProductAdapter : ListAdapter(ProductDiffUtil), HitsView { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ProductViewHolder(parent.inflate(R.layout.list_item_large)) + + override fun onBindViewHolder(holder: ProductViewHolder, position: Int) = + holder.bind(getItem(position)) + + override fun setHits(hits: List) = submitList(hits) + + class ProductViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + + fun bind(item: Product) { + view.findViewById(R.id.itemTitle).text = + item.highlightedName?.toSpannedString() ?: item.name + view.findViewById(R.id.itemSubtitle).text = item.price.value + view.findViewById(R.id.itemImage).load(item.images.first()) + } + } + + private object ProductDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Product, newItem: Product) = oldItem.objectID == newItem.objectID + override fun areContentsTheSame(oldItem: Product, newItem: Product) = oldItem == newItem + } +} diff --git a/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductFragment.kt b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductFragment.kt new file mode 100644 index 000000000..f8686f0e3 --- /dev/null +++ b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/ProductFragment.kt @@ -0,0 +1,38 @@ +package com.algolia.instantsearch.examples.android.guides.querysuggestion + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.RecyclerView +import com.algolia.instantsearch.core.connection.ConnectionHandler +import com.algolia.instantsearch.core.hits.connectHitsView +import com.algolia.instantsearch.examples.android.R +import com.algolia.instantsearch.examples.android.guides.extension.configure +import com.algolia.search.helper.deserialize + +class ProductFragment : Fragment(R.layout.fragment_items) { + + private val viewModel: QuerySuggestionViewModel by activityViewModels() + private val connection = ConnectionHandler() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Configure products view + val productAdapter = ProductAdapter() + view.findViewById(R.id.items) + .configure(productAdapter) // Configure the RecyclerView with the adapter + connection += viewModel.productSearcher.connectHitsView(productAdapter) { + it.hits.deserialize(Product.serializer()) + } + + // Run initial search + viewModel.productSearcher.searchAsync() + } + + override fun onDestroyView() { + super.onDestroyView() + connection.clear() + } +} diff --git a/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionActivity.kt b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionActivity.kt new file mode 100644 index 000000000..5af1c197b --- /dev/null +++ b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionActivity.kt @@ -0,0 +1,60 @@ +package com.algolia.instantsearch.examples.android.guides.querysuggestion + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.commit +import androidx.fragment.app.replace +import com.algolia.instantsearch.android.searchbox.SearchBoxViewAppCompat +import com.algolia.instantsearch.core.connection.ConnectionHandler +import com.algolia.instantsearch.examples.android.R +import com.algolia.instantsearch.searchbox.connectView + +class QuerySuggestionActivity : AppCompatActivity() { + + private val viewModel by viewModels() + private val connection = ConnectionHandler() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_query_suggestion) + + // Setup search box + val searchView = findViewById(R.id.searchView) + val searchBoxView = SearchBoxViewAppCompat(searchView) + connection += viewModel.searchBox.connectView(searchBoxView) + + // Switch fragments on search box focus + searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (hasFocus) showSuggestions() else showProducts() + } + + // Observe suggestions + viewModel.suggestions.observe(this) { searchBoxView.setText(it.query, true) } + + // Initially show products view + showProducts() + } + + private fun showSuggestions() { + supportFragmentManager.commit { + replace(R.id.container) + setReorderingAllowed(true) + addToBackStack("suggestions") // name can be null + } + } + + private fun showProducts() { + supportFragmentManager.commit { + replace(R.id.container) + setReorderingAllowed(true) + addToBackStack("products") // name can be null + } + } + + override fun onDestroy() { + super.onDestroy() + connection.clear() + } +} diff --git a/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionViewModel.kt b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionViewModel.kt new file mode 100644 index 000000000..c44bd1e17 --- /dev/null +++ b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/QuerySuggestionViewModel.kt @@ -0,0 +1,41 @@ +package com.algolia.instantsearch.examples.android.guides.querysuggestion + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.algolia.instantsearch.core.connection.ConnectionHandler +import com.algolia.instantsearch.examples.android.guides.model.Suggestion +import com.algolia.instantsearch.searchbox.SearchBoxConnector +import com.algolia.instantsearch.searcher.hits.addHitsSearcher +import com.algolia.instantsearch.searcher.multi.MultiSearcher +import com.algolia.search.client.ClientSearch +import com.algolia.search.logging.LogLevel +import com.algolia.search.model.APIKey +import com.algolia.search.model.ApplicationID +import com.algolia.search.model.IndexName + +class QuerySuggestionViewModel : ViewModel() { + + private val client = ClientSearch( + applicationID = ApplicationID("latency"), + apiKey = APIKey("927c3fe76d4b52c5a2912973f35a3077"), + logLevel = LogLevel.All + ) + val multiSearcher = MultiSearcher(client) + val productSearcher = multiSearcher.addHitsSearcher(indexName = IndexName("STAGING_native_ecom_demo_products")) + val suggestionSearcher = + multiSearcher.addHitsSearcher(indexName = IndexName("STAGING_native_ecom_demo_products_query_suggestions")) + val searchBox = SearchBoxConnector(multiSearcher) + val suggestions = MutableLiveData() + val connections = ConnectionHandler() + + init { + searchBox.connect() + } + + override fun onCleared() { + searchBox.disconnect() + connections.clear() + multiSearcher.cancel() + client.close() + } +} diff --git a/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionAdapter.kt b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionAdapter.kt new file mode 100644 index 000000000..19a33b8c6 --- /dev/null +++ b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionAdapter.kt @@ -0,0 +1,45 @@ +package com.algolia.instantsearch.examples.android.guides.querysuggestion + +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.algolia.instantsearch.android.highlighting.toSpannedString +import com.algolia.instantsearch.android.inflate +import com.algolia.instantsearch.core.hits.HitsView +import com.algolia.instantsearch.examples.android.R +import com.algolia.instantsearch.examples.android.guides.model.Suggestion +import com.algolia.instantsearch.examples.android.guides.querysuggestion.SuggestionAdapter.SuggestionViewHolder + +class SuggestionAdapter(private val onSuggestionClick: ((Suggestion) -> Unit)) : + ListAdapter(SuggestionAdapter), + HitsView { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + SuggestionViewHolder(parent.inflate(R.layout.list_item_suggestion)) + + override fun onBindViewHolder(holder: SuggestionViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item, onSuggestionClick) + } + + override fun setHits(hits: List) = submitList(hits) + + class SuggestionViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + + fun bind(item: Suggestion, onClick: ((Suggestion) -> Unit)) { + view.setOnClickListener { onClick(item) } + view.findViewById(R.id.itemName).text = item.highlightedQuery?.toSpannedString() ?: item.query + } + } + + companion object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Suggestion, newItem: Suggestion) = + oldItem.objectID == newItem.objectID + + override fun areContentsTheSame(oldItem: Suggestion, newItem: Suggestion): Boolean = + oldItem == newItem + } +} diff --git a/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionFragment.kt b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionFragment.kt new file mode 100644 index 000000000..79473c9b3 --- /dev/null +++ b/examples/android/src/main/kotlin/com/algolia/instantsearch/examples/android/guides/querysuggestion/SuggestionFragment.kt @@ -0,0 +1,40 @@ +package com.algolia.instantsearch.examples.android.guides.querysuggestion + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.RecyclerView +import com.algolia.instantsearch.core.connection.ConnectionHandler +import com.algolia.instantsearch.core.hits.connectHitsView +import com.algolia.instantsearch.examples.android.R +import com.algolia.instantsearch.examples.android.guides.extension.configure +import com.algolia.instantsearch.examples.android.guides.model.Suggestion +import com.algolia.search.helper.deserialize + +class SuggestionFragment : Fragment(R.layout.fragment_items) { + + private val viewModel: QuerySuggestionViewModel by activityViewModels() + private val connection = ConnectionHandler() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // Configure suggestions view + val suggestionAdapter = SuggestionAdapter { + // On suggestion click, update the + viewModel.suggestions.value = it + } + view.findViewById(R.id.items).configure(suggestionAdapter) // Configure the RecyclerView with the adapter + connection += viewModel.suggestionSearcher.connectHitsView(suggestionAdapter) { + it.hits.deserialize(Suggestion.serializer()) + } + + // Run initial search + viewModel.suggestionSearcher.searchAsync() + } + + override fun onDestroyView() { + super.onDestroyView() + connection.clear() + } +}