Skip to content
This repository was archived by the owner on May 24, 2022. It is now read-only.

Commit 6341a29

Browse files
authored
feat: code exchange examples (#43)
1 parent e825513 commit 6341a29

File tree

184 files changed

+4095
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

184 files changed

+4095
-11
lines changed

build.gradle

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ buildscript {
22
ext.kotlin_version = "1.5.31"
33
ext.lifecycle_version = "2.2.0"
44
ext.build_tools_version = "31.0.0"
5-
ext.instantsearch = '2.11.3'
5+
ext.instantsearch = '2.11.5-SNAPSHOT'
66
ext.canary = '2.7'
77
ext.compose_version = '1.0.5'
88
repositories {
99
mavenCentral()
1010
google()
1111
}
1212
dependencies {
13-
classpath 'com.android.tools.build:gradle:7.0.3'
13+
classpath 'com.android.tools.build:gradle:7.0.4'
1414
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1515
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
1616
}
@@ -20,6 +20,7 @@ allprojects {
2020
repositories {
2121
mavenCentral()
2222
google()
23+
maven { url = "https://oss.sonatype.org/content/repositories/snapshots/" }
2324
}
2425
}
2526

compose/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
6161
kotlinOptions {
6262
freeCompilerArgs += [
6363
'-Xopt-in=kotlin.RequiresOptIn',
64-
'-Xopt-in=com.algolia.instantsearch.core.ExperimentalInstantSearch',
64+
'-Xopt-in=com.algolia.instantsearch.ExperimentalInstantSearch',
6565
]
6666
}
6767
}

exchange/categories-hits/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Categories & Hits implementation example
2+
3+
Search experience consisting of two results sections:
4+
- Products categories lists
5+
- Products list
6+
7+
Demonstrates simultaneous search for hits and facet values.
8+
9+
<img src="./demo.gif" width="300"/>

exchange/categories-hits/build.gradle

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
plugins {
2+
id 'com.android.application'
3+
id 'kotlin-android'
4+
id 'kotlinx-serialization'
5+
}
6+
7+
android {
8+
compileSdk 31
9+
buildToolsVersion build_tools_version
10+
11+
defaultConfig {
12+
applicationId "com.algolia.exchange.categories.hits"
13+
minSdk 23
14+
targetSdk 31
15+
versionCode 1
16+
versionName "1.0"
17+
}
18+
compileOptions {
19+
sourceCompatibility JavaVersion.VERSION_1_8
20+
targetCompatibility JavaVersion.VERSION_1_8
21+
}
22+
buildFeatures {
23+
compose true
24+
}
25+
composeOptions {
26+
kotlinCompilerExtensionVersion compose_version
27+
}
28+
}
29+
30+
dependencies {
31+
implementation "com.algolia:instantsearch-compose:$instantsearch"
32+
implementation "androidx.compose.ui:ui:$compose_version"
33+
implementation "androidx.compose.ui:ui-tooling:$compose_version"
34+
implementation "androidx.compose.material:material:$compose_version"
35+
implementation "androidx.compose.material:material-icons-extended:$compose_version"
36+
implementation 'androidx.activity:activity-compose:1.4.0'
37+
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
38+
implementation "io.coil-kt:coil-compose:1.4.0"
39+
}
40+
41+
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
42+
kotlinOptions {
43+
freeCompilerArgs += [
44+
'-Xopt-in=kotlin.RequiresOptIn',
45+
'-Xopt-in=com.algolia.instantsearch.ExperimentalInstantSearch',
46+
'-Xopt-in=androidx.compose.material.ExperimentalMaterialApi',
47+
]
48+
}
49+
}

exchange/categories-hits/demo.gif

1.6 MB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.algolia.exchange.query.categories.hits">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:icon="@mipmap/ic_launcher"
8+
android:label="@string/app_name"
9+
android:roundIcon="@mipmap/ic_launcher_round"
10+
android:supportsRtl="true"
11+
android:theme="@style/Theme.App">
12+
<activity
13+
android:name=".MainActivity"
14+
android:exported="true"
15+
android:label="@string/app_name"
16+
android:theme="@style/Theme.App.NoActionBar">
17+
<intent-filter>
18+
<action android:name="android.intent.action.MAIN" />
19+
20+
<category android:name="android.intent.category.LAUNCHER" />
21+
</intent-filter>
22+
</activity>
23+
</application>
24+
25+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.algolia.exchange.query.categories.hits
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.viewModels
7+
import androidx.compose.material.MaterialTheme
8+
9+
class MainActivity : ComponentActivity() {
10+
11+
private val viewModel: MainViewModel by viewModels()
12+
13+
override fun onCreate(savedInstanceState: Bundle?) {
14+
super.onCreate(savedInstanceState)
15+
setContent {
16+
MaterialTheme {
17+
SearchScreen(
18+
searchBoxState = viewModel.searchBoxState,
19+
hitsState = viewModel.hitsState,
20+
categoriesState = viewModel.categoriesState,
21+
)
22+
}
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.algolia.exchange.query.categories.hits
2+
3+
import androidx.lifecycle.ViewModel
4+
import com.algolia.instantsearch.compose.hits.HitsState
5+
import com.algolia.instantsearch.compose.searchbox.SearchBoxState
6+
import com.algolia.instantsearch.core.connection.ConnectionHandler
7+
import com.algolia.instantsearch.core.hits.connectHitsView
8+
import com.algolia.instantsearch.helper.searchbox.SearchBoxConnector
9+
import com.algolia.instantsearch.helper.searchbox.connectView
10+
import com.algolia.instantsearch.helper.searcher.facets.addFacetsSearcher
11+
import com.algolia.instantsearch.helper.searcher.hits.addHitsSearcher
12+
import com.algolia.instantsearch.helper.searcher.multi.MultiSearcher
13+
import com.algolia.search.client.ClientSearch
14+
import com.algolia.search.helper.deserialize
15+
import com.algolia.search.model.APIKey
16+
import com.algolia.search.model.ApplicationID
17+
import com.algolia.search.model.Attribute
18+
import com.algolia.search.model.IndexName
19+
import com.algolia.search.model.search.Facet
20+
import io.ktor.client.features.logging.*
21+
22+
class MainViewModel : ViewModel() {
23+
24+
private val client = ClientSearch(
25+
ApplicationID("latency"),
26+
APIKey("afc3dd66dd1293e2e2736a5a51b05c0a"),
27+
LogLevel.ALL
28+
)
29+
private val indexName = IndexName("instant_search")
30+
private val attribute = Attribute("categories")
31+
private val multiSearcher = MultiSearcher(client)
32+
private val suggestionsSearcher = multiSearcher.addHitsSearcher(indexName)
33+
private val categoriesSearcher = multiSearcher.addFacetsSearcher(indexName, attribute)
34+
private val searchBoxConnector = SearchBoxConnector(multiSearcher)
35+
private val connections = ConnectionHandler(searchBoxConnector)
36+
37+
val searchBoxState = SearchBoxState()
38+
val categoriesState = HitsState<Facet>()
39+
val hitsState = HitsState<Product>()
40+
41+
init {
42+
connections += searchBoxConnector.connectView(searchBoxState)
43+
connections += categoriesSearcher.connectHitsView(categoriesState) { it.facets }
44+
connections += suggestionsSearcher.connectHitsView(hitsState) { it.hits.deserialize(Product.serializer()) }
45+
multiSearcher.searchAsync()
46+
}
47+
48+
override fun onCleared() {
49+
super.onCleared()
50+
multiSearcher.cancel()
51+
connections.clear()
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.algolia.exchange.query.categories.hits
2+
3+
import com.algolia.search.model.ObjectID
4+
import com.algolia.search.model.indexing.Indexable
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
data class Product(
9+
val name: String,
10+
val description: String,
11+
val image: String,
12+
override val objectID: ObjectID
13+
) : Indexable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.algolia.exchange.query.categories.hits
2+
3+
import android.R
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.layout.*
7+
import androidx.compose.foundation.rememberScrollState
8+
import androidx.compose.foundation.verticalScroll
9+
import androidx.compose.material.Icon
10+
import androidx.compose.material.MaterialTheme
11+
import androidx.compose.material.Text
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.filled.Category
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.layout.ContentScale
17+
import androidx.compose.ui.text.AnnotatedString
18+
import androidx.compose.ui.text.style.TextOverflow
19+
import androidx.compose.ui.unit.dp
20+
import coil.compose.rememberImagePainter
21+
import com.algolia.instantsearch.compose.highlighting.toAnnotatedString
22+
import com.algolia.instantsearch.compose.hits.HitsState
23+
import com.algolia.instantsearch.compose.searchbox.SearchBox
24+
import com.algolia.instantsearch.compose.searchbox.SearchBoxState
25+
import com.algolia.instantsearch.core.highlighting.DefaultPostTag
26+
import com.algolia.instantsearch.core.highlighting.DefaultPreTag
27+
import com.algolia.instantsearch.core.highlighting.HighlightTokenizer
28+
import com.algolia.search.model.search.Facet
29+
30+
@Composable
31+
fun SearchScreen(
32+
modifier: Modifier = Modifier,
33+
searchBoxState: SearchBoxState,
34+
hitsState: HitsState<Product>,
35+
categoriesState: HitsState<Facet>,
36+
) {
37+
val scrollState = rememberScrollState()
38+
Column(
39+
modifier = modifier
40+
.fillMaxWidth()
41+
.verticalScroll(scrollState)
42+
) {
43+
SearchBox(
44+
modifier = Modifier
45+
.fillMaxWidth()
46+
.padding(12.dp),
47+
searchBoxState = searchBoxState,
48+
)
49+
50+
Categories(categories = categoriesState.hits)
51+
Products(products = hitsState.hits)
52+
}
53+
}
54+
55+
@Composable
56+
private fun Categories(
57+
modifier: Modifier = Modifier,
58+
categories: List<Facet>,
59+
) {
60+
Column(modifier) {
61+
SectionTitle(
62+
modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 4.dp),
63+
title = "Categories"
64+
)
65+
categories.forEach { category ->
66+
CategoryRow(category = category)
67+
}
68+
}
69+
}
70+
71+
@Composable
72+
private fun CategoryRow(
73+
modifier: Modifier = Modifier,
74+
category: Facet
75+
) {
76+
Row(
77+
modifier
78+
.background(MaterialTheme.colors.surface)
79+
.fillMaxWidth()
80+
.padding(horizontal = 24.dp, vertical = 12.dp)
81+
) {
82+
Icon(
83+
imageVector = Icons.Default.Category,
84+
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.2f),
85+
contentDescription = null
86+
)
87+
Text(
88+
text = category.highlighted.toAnnotatedString(),
89+
modifier = Modifier.padding(start = 12.dp),
90+
)
91+
}
92+
}
93+
94+
95+
@Composable
96+
private fun Products(products: List<Product>) {
97+
SectionTitle(
98+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
99+
title = "Products"
100+
)
101+
products.forEach { product ->
102+
ProductRow(product = product)
103+
}
104+
}
105+
106+
@Composable
107+
private fun ProductRow(modifier: Modifier = Modifier, product: Product) {
108+
Row(
109+
modifier
110+
.background(MaterialTheme.colors.surface)
111+
.padding(12.dp)
112+
) {
113+
Image(
114+
modifier = Modifier.size(64.dp),
115+
contentScale = ContentScale.Crop,
116+
painter = rememberImagePainter(
117+
data = product.image,
118+
builder = {
119+
placeholder(R.drawable.ic_menu_report_image)
120+
error(R.drawable.ic_menu_report_image)
121+
},
122+
),
123+
contentDescription = product.name,
124+
)
125+
126+
Column(Modifier.padding(start = 8.dp)) {
127+
Text(
128+
text = product.name,
129+
maxLines = 2,
130+
overflow = TextOverflow.Ellipsis,
131+
style = MaterialTheme.typography.subtitle2
132+
)
133+
Text(
134+
text = product.description,
135+
maxLines = 2,
136+
overflow = TextOverflow.Ellipsis,
137+
style = MaterialTheme.typography.caption,
138+
)
139+
}
140+
}
141+
}
142+
143+
@Composable
144+
private fun SectionTitle(modifier: Modifier = Modifier, title: String) {
145+
Text(
146+
modifier = modifier,
147+
text = title, style = MaterialTheme.typography.subtitle2,
148+
color = MaterialTheme.colors.onBackground.copy(alpha = 0.4f),
149+
)
150+
}
151+
152+
private fun String.toAnnotatedString(): AnnotatedString {
153+
return HighlightTokenizer(DefaultPreTag, DefaultPostTag)(this).toAnnotatedString()
154+
}

0 commit comments

Comments
 (0)