Skip to content

Commit a11ba78

Browse files
authored
Demo app: Recommendations (open-telemetry#554)
* Screen with shipping and payment info after clicking checkout. * cleaned up InfoScreen with reusable components to reduce code repetition * CVV fields displays dots insted of numbers and Proceed button is disabled untill all fields aren't blank. Added kotlin.reflect to dependencies in order to check if all fields of a data class aren't blank. * removed the use of reflect * changed the app title and added a top-left navigate up arrow in product details and checkout info screens * unused import * starting developing recommended products section in the product details screen * added a recommendation service that recommends random products that aren't selected and aren't in a cart (like in the desktop demo) * small cleanup * adjusting font size in the smaller product card version * moved getting list of all products and products in cart into the recommedation service to avoid passing lists of products around * anused import * small code refactor and number of recommended product added as an optional argument of getRecommendedProducts * formatting * moved getAllNonCartProducts for better code organization
1 parent f41f908 commit a11ba78

File tree

9 files changed

+328
-135
lines changed

9 files changed

+328
-135
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.opentelemetry.android.demo.clients
2+
3+
import io.opentelemetry.android.demo.model.Product
4+
import io.opentelemetry.android.demo.ui.shop.cart.CartViewModel
5+
6+
class RecommendationService(
7+
private val productCatalogClient: ProductCatalogClient,
8+
private val cartViewModel: CartViewModel
9+
) {
10+
11+
fun getRecommendedProducts(currentProduct: Product, numberOfProducts: Int = 4): List<Product> {
12+
return getAllNonCartProducts().filter { it.id != currentProduct.id }
13+
.shuffled().take(numberOfProducts)
14+
}
15+
16+
fun getRecommendedProducts(numberOfProducts: Int = 4): List<Product> {
17+
return getAllNonCartProducts().shuffled().take(numberOfProducts)
18+
}
19+
20+
private fun getAllNonCartProducts(): List<Product>{
21+
val allProducts = productCatalogClient.get()
22+
val cartItems = cartViewModel.cartItems.value
23+
24+
return allProducts.filter { product -> cartItems.none { it.product.id == product.id } }
25+
}
26+
}
27+

demo-app/src/main/java/io/opentelemetry/android/demo/ui/shop/AstronomyShopActivity.kt

+14-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import io.opentelemetry.android.demo.ui.shop.cart.InfoScreen
3636
class AstronomyShopActivity : AppCompatActivity() {
3737
override fun onCreate(savedInstanceState: Bundle?) {
3838
super.onCreate(savedInstanceState)
39+
title = "Astronomy Shop"
3940
setContent {
4041
AstronomyShopScreen()
4142
}
@@ -84,15 +85,25 @@ fun AstronomyShopScreen() {
8485
}
8586
}
8687
composable(BottomNavItem.Cart.route) {
87-
CartScreen(cartViewModel = cartViewModel) { astronomyShopNavController.navigateToCheckoutInfo() }
88+
CartScreen(cartViewModel = cartViewModel, onCheckoutClick = {astronomyShopNavController.navigateToCheckoutInfo()}, onProductClick = { productId ->
89+
astronomyShopNavController.navigateToProductDetail(productId)
90+
})
8891
}
8992
composable("${MainDestinations.PRODUCT_DETAIL_ROUTE}/{${MainDestinations.PRODUCT_ID_KEY}}") { backStackEntry ->
9093
val productId = backStackEntry.arguments?.getString(MainDestinations.PRODUCT_ID_KEY)
9194
val product = products.find { it.id == productId }
92-
product?.let { ProductDetails(product = it, cartViewModel) }
95+
product?.let { ProductDetails(
96+
product = it,
97+
cartViewModel,
98+
upPress = {astronomyShopNavController.upPress()},
99+
onProductClick = { productId ->
100+
astronomyShopNavController.navigateToProductDetail(productId)
101+
}
102+
)
103+
}
93104
}
94105
composable(MainDestinations.CHECKOUT_INFO_ROUTE) {
95-
InfoScreen()
106+
InfoScreen(upPress = {astronomyShopNavController.upPress()})
96107
}
97108
}
98109
}

demo-app/src/main/java/io/opentelemetry/android/demo/ui/shop/cart/Cart.kt

+58-43
Original file line numberDiff line numberDiff line change
@@ -8,70 +8,85 @@ import androidx.compose.ui.Modifier
88
import java.util.Locale
99
import androidx.compose.ui.unit.dp
1010
import androidx.lifecycle.viewmodel.compose.viewModel
11-
import io.opentelemetry.android.demo.model.Product
1211
import io.opentelemetry.android.demo.ui.shop.products.ProductCard
1312
import androidx.compose.ui.Alignment
1413
import androidx.compose.ui.graphics.Color
14+
import androidx.compose.ui.platform.LocalContext
15+
import io.opentelemetry.android.demo.clients.ProductCatalogClient
16+
import io.opentelemetry.android.demo.clients.RecommendationService
17+
import io.opentelemetry.android.demo.ui.shop.products.RecommendedSection
1518

1619
@Composable
17-
fun CartScreen(cartViewModel: CartViewModel = viewModel(),
18-
onCheckoutClick: () -> Unit
20+
fun CartScreen(
21+
cartViewModel: CartViewModel = viewModel(),
22+
onCheckoutClick: () -> Unit,
23+
onProductClick: (String) -> Unit
1924
) {
25+
val context = LocalContext.current
26+
val productsClient = ProductCatalogClient(context)
27+
val recommendationService = remember { RecommendationService(productsClient, cartViewModel) }
2028
val cartItems by cartViewModel.cartItems.collectAsState()
2129
val isCartEmpty = cartItems.isEmpty()
30+
val recommendedProducts = remember { recommendationService.getRecommendedProducts() }
2231

23-
Column(
32+
33+
LazyColumn(
2434
modifier = Modifier
2535
.fillMaxSize()
2636
.padding(16.dp)
2737
) {
28-
Box(
29-
modifier = Modifier.fillMaxWidth(),
30-
contentAlignment = Alignment.TopEnd
31-
) {
32-
OutlinedButton(
33-
onClick = { cartViewModel.clearCart() },
34-
modifier = Modifier
38+
item {
39+
Box(
40+
modifier = Modifier.fillMaxWidth(),
41+
contentAlignment = Alignment.TopEnd
3542
) {
36-
Text("Empty Cart", color = Color.Red)
43+
OutlinedButton(
44+
onClick = { cartViewModel.clearCart() },
45+
modifier = Modifier
46+
) {
47+
Text("Empty Cart", color = Color.Red)
48+
}
3749
}
3850
}
3951

40-
LazyColumn(modifier = Modifier.weight(1f)) {
41-
items(cartItems.size) { index ->
42-
ProductCard(product = cartItems[index].product, onClick = {})
43-
Spacer(modifier = Modifier.height(8.dp))
44-
Text(
45-
text = "Quantity: ${cartItems[index].quantity}",
46-
modifier = Modifier.padding(start = 16.dp)
47-
)
48-
Spacer(modifier = Modifier.height(8.dp))
49-
Text(
50-
text = "Total: \$${String.format(Locale.US, "%.2f", cartItems[index].totalPrice())}",
51-
modifier = Modifier.padding(start = 16.dp)
52-
)
53-
}
52+
items(cartItems.size) { index ->
53+
ProductCard(product = cartItems[index].product, onProductClick = {})
54+
Spacer(modifier = Modifier.height(8.dp))
55+
Text(
56+
text = "Quantity: ${cartItems[index].quantity}",
57+
modifier = Modifier.padding(start = 16.dp)
58+
)
59+
Spacer(modifier = Modifier.height(8.dp))
60+
Text(
61+
text = "Total: \$${String.format(Locale.US, "%.2f", cartItems[index].totalPrice())}",
62+
modifier = Modifier.padding(start = 16.dp)
63+
)
64+
Spacer(modifier = Modifier.height(16.dp))
5465
}
5566

56-
Spacer(modifier = Modifier.height(16.dp))
67+
item {
68+
Spacer(modifier = Modifier.height(16.dp))
69+
70+
Text(
71+
text = "Total Price: \$${String.format(Locale.US, "%.2f", cartViewModel.getTotalPrice())}",
72+
modifier = Modifier.padding(16.dp)
73+
)
5774

58-
Text(
59-
text = "Total Price: \$${String.format(Locale.US, "%.2f", cartViewModel.getTotalPrice())}",
60-
modifier = Modifier.padding(16.dp)
61-
)
75+
Button(
76+
onClick = onCheckoutClick,
77+
enabled = !isCartEmpty,
78+
colors = ButtonDefaults.buttonColors(
79+
containerColor = if (isCartEmpty) Color.Gray else MaterialTheme.colorScheme.primary
80+
),
81+
modifier = Modifier
82+
.fillMaxWidth()
83+
.padding(16.dp)
84+
) {
85+
Text("Checkout")
86+
}
6287

63-
Button(
64-
onClick = onCheckoutClick,
65-
enabled = !isCartEmpty,
66-
colors = ButtonDefaults.buttonColors(
67-
containerColor = if (isCartEmpty) Color.Gray else MaterialTheme.colorScheme.primary
68-
),
69-
modifier = Modifier
70-
.fillMaxWidth()
71-
.padding(16.dp)
72-
) {
73-
Text("Checkout")
88+
Spacer(modifier = Modifier.height(32.dp))
89+
RecommendedSection(recommendedProducts = recommendedProducts, onProductClick = onProductClick)
7490
}
7591
}
7692
}
77-

demo-app/src/main/java/io/opentelemetry/android/demo/ui/shop/cart/CheckoutInfo.kt

+55-35
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import androidx.compose.ui.text.input.KeyboardType
1515
import androidx.compose.ui.text.input.PasswordVisualTransformation
1616
import androidx.compose.ui.text.input.VisualTransformation
1717
import androidx.compose.ui.unit.dp
18+
import io.opentelemetry.android.demo.ui.shop.components.UpPressButton
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.text.style.TextAlign
1821

1922
data class ShippingInfo(
2023
var email: String = "",
@@ -66,7 +69,10 @@ fun SectionHeader(title: String) {
6669
Text(
6770
text = title,
6871
style = MaterialTheme.typography.titleLarge,
69-
modifier = Modifier.padding(vertical = 8.dp)
72+
textAlign = TextAlign.Center,
73+
modifier = Modifier
74+
.padding(vertical = 8.dp)
75+
.fillMaxWidth()
7076
)
7177
}
7278

@@ -92,56 +98,70 @@ fun InfoFieldsSection(
9298
}
9399

94100
@Composable
95-
fun InfoScreen() {
101+
fun InfoScreen(
102+
upPress: () -> Unit
103+
) {
96104
var shippingInfo by remember { mutableStateOf(ShippingInfo()) }
97105
var paymentInfo by remember { mutableStateOf(PaymentInfo()) }
98106

99107
val focusManager = LocalFocusManager.current
100-
101108
val canProceed = shippingInfo.isComplete() && paymentInfo.isComplete()
102109

103-
Column(
110+
Box(
104111
modifier = Modifier
105112
.fillMaxSize()
106-
.padding(16.dp)
107113
.background(Color.White)
108-
.clickable { focusManager.clearFocus() }
109-
.verticalScroll(rememberScrollState())
110114
) {
111-
SectionHeader(title = "Shipping Address")
112-
113-
InfoFieldsSection(
114-
fields = listOf(
115-
Triple("E-mail Address", shippingInfo.email) { shippingInfo = shippingInfo.copy(email = it) },
116-
Triple("Street Address", shippingInfo.streetAddress) { shippingInfo = shippingInfo.copy(streetAddress = it) },
117-
Triple("Zip Code", shippingInfo.zipCode) { shippingInfo = shippingInfo.copy(zipCode = it) },
118-
Triple("City", shippingInfo.city) { shippingInfo = shippingInfo.copy(city = it) },
119-
Triple("State", shippingInfo.state) { shippingInfo = shippingInfo.copy(state = it) },
120-
Triple("Country", shippingInfo.country) { shippingInfo = shippingInfo.copy(country = it) }
115+
// Content inside a Column
116+
Column(
117+
modifier = Modifier
118+
.fillMaxSize()
119+
.padding(16.dp)
120+
.clickable { focusManager.clearFocus() }
121+
.verticalScroll(rememberScrollState())
122+
) {
123+
SectionHeader(title = "Shipping Address")
124+
125+
InfoFieldsSection(
126+
fields = listOf(
127+
Triple("E-mail Address", shippingInfo.email) { shippingInfo = shippingInfo.copy(email = it) },
128+
Triple("Street Address", shippingInfo.streetAddress) { shippingInfo = shippingInfo.copy(streetAddress = it) },
129+
Triple("Zip Code", shippingInfo.zipCode) { shippingInfo = shippingInfo.copy(zipCode = it) },
130+
Triple("City", shippingInfo.city) { shippingInfo = shippingInfo.copy(city = it) },
131+
Triple("State", shippingInfo.state) { shippingInfo = shippingInfo.copy(state = it) },
132+
Triple("Country", shippingInfo.country) { shippingInfo = shippingInfo.copy(country = it) }
133+
)
121134
)
122-
)
123135

124-
Spacer(modifier = Modifier.height(16.dp))
136+
Spacer(modifier = Modifier.height(16.dp))
125137

126-
SectionHeader(title = "Payment Method")
138+
SectionHeader(title = "Payment Method")
127139

128-
InfoFieldsSection(
129-
fields = listOf(
130-
Triple("Credit Card Number", paymentInfo.creditCardNumber) { paymentInfo = paymentInfo.copy(creditCardNumber = it) },
131-
Triple("Month", paymentInfo.expiryMonth) { paymentInfo = paymentInfo.copy(expiryMonth = it) },
132-
Triple("Year", paymentInfo.expiryYear) { paymentInfo = paymentInfo.copy(expiryYear = it) },
133-
Triple("CVV", paymentInfo.cvv) { paymentInfo = paymentInfo.copy(cvv = it) }
140+
InfoFieldsSection(
141+
fields = listOf(
142+
Triple("Credit Card Number", paymentInfo.creditCardNumber) { paymentInfo = paymentInfo.copy(creditCardNumber = it) },
143+
Triple("Month", paymentInfo.expiryMonth) { paymentInfo = paymentInfo.copy(expiryMonth = it) },
144+
Triple("Year", paymentInfo.expiryYear) { paymentInfo = paymentInfo.copy(expiryYear = it) },
145+
Triple("CVV", paymentInfo.cvv) { paymentInfo = paymentInfo.copy(cvv = it) }
146+
)
134147
)
135-
)
136148

137-
Spacer(modifier = Modifier.height(16.dp))
149+
Spacer(modifier = Modifier.height(16.dp))
138150

139-
Button(
140-
onClick = { /*TODO Handle*/ },
141-
modifier = Modifier.fillMaxWidth(),
142-
enabled = canProceed
143-
) {
144-
Text("Proceed")
151+
Button(
152+
onClick = { /*TODO Handle*/ },
153+
modifier = Modifier.fillMaxWidth(),
154+
enabled = canProceed
155+
) {
156+
Text("Proceed")
157+
}
145158
}
159+
160+
UpPressButton(
161+
upPress = upPress,
162+
modifier = Modifier
163+
.align(Alignment.TopStart)
164+
.padding(8.dp)
165+
)
146166
}
147-
}
167+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.opentelemetry.android.demo.ui.shop.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.size
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.foundation.shape.CircleShape
8+
import androidx.compose.material3.Icon
9+
import androidx.compose.material.icons.Icons
10+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.graphics.Color
14+
import androidx.compose.ui.unit.dp
15+
import androidx.compose.ui.zIndex
16+
17+
@Composable
18+
fun UpPressButton(
19+
upPress: () -> Unit,
20+
modifier: Modifier = Modifier
21+
) {
22+
Icon(
23+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
24+
contentDescription = "Navigate Up",
25+
tint = Color.Black,
26+
modifier = modifier
27+
.size(48.dp)
28+
.background(Color.White, shape = CircleShape)
29+
.padding(8.dp)
30+
.zIndex(1f)
31+
.clickable(onClick = upPress)
32+
)
33+
}

0 commit comments

Comments
 (0)