Skip to content

Commit

Permalink
[FEATURE] #33-Product Detail UI 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Myungjun Hyun authored and myung6024 committed Jul 3, 2023
1 parent 1b0cf85 commit bb917fa
Show file tree
Hide file tree
Showing 25 changed files with 851 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
/captures
.externalNativeBuild
.cxx
local.properties
local.properties
feature/evaluate/build/
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
implementation(project(":feature:login"))
implementation(project(":feature:evaluate"))
implementation(project(":feature:main"))
implementation(project(":feature:detail"))

implementation(libs.androidx.core.splashscreen)
}
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
</intent-filter>
</activity>
</application>
</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
// Instrumented tests: jUnit rules and runners
"androidTestImplementation"(libs.findLibrary("androidx.test.ext.junit").get())
"androidTestImplementation"(libs.findLibrary("androidx.test.runner").get())
"androidTestImplementation"(libs.findLibrary("androidx.recyclerview").get())
"implementation"(libs.findLibrary("androidx.recyclerview").get())
"implementation"(libs.findLibrary("androidx.appcompat").get())
}
}
Expand Down
1 change: 1 addition & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
<string name="item_product_event">행사중</string>
<string name="item_product_recommended_percentage">%.1f%%</string>
<string name="item_product_review_count">리뷰 %d개</string>
<string name="item_recent_review_user_and_date">%s · %s</string>
</resources>
1 change: 1 addition & 0 deletions feature/detail/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
22 changes: 22 additions & 0 deletions feature/detail/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id("peonlee.android.feature")
}

android {
namespace = "com.peonlee.feature.detail"

viewBinding {
enable = true
}
}

dependencies {
implementation(project(":core:ui"))
implementation(project(":core:common"))
implementation(project(":core:domain"))
implementation(project(":core:data"))
implementation(libs.androidx.constraintlayout)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.coil)
implementation(libs.androidx.cardview)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.peonlee.feature.detail

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.peonlee.feature.detail", appContext.packageName)
}
}
14 changes: 14 additions & 0 deletions feature/detail/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:supportsRtl="true">
<activity
android:name=".ProductDetailActivity"
android:exported="false"
android:label="@string/title_activity_product_detail"
android:theme="@style/Product.Detail" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.peonlee.feature.detail

import com.peonlee.core.ui.base.BaseActivity
import com.peonlee.feature.detail.databinding.ActivityProductDetailBinding

class ProductDetailActivity : BaseActivity<ActivityProductDetailBinding>() {
private val adapter by lazy { ProductDetailListAdapter() }
override fun bindingFactory(): ActivityProductDetailBinding = ActivityProductDetailBinding.inflate(layoutInflater)

override fun initViews() {
binding.rvProductDetail.adapter = adapter
adapter.submitList(
listOf(
ProductDetailListItem.Product(
id = 0,
imageUrl = "https://cdn.pixabay.com/photo/2019/04/04/15/17/smartphone-4103051_1280.jpg",
productName = "Test",
price = 2000,
upvoteRate = 3,
reviewCount = 2,
eventList = listOf(
ProductDetailListItem.Event(imageUrl = "", title = "1+1"),
ProductDetailListItem.Event(imageUrl = "", title = "덤 증정"),
ProductDetailListItem.Event(imageUrl = "", title = "덤 증정")
)
),
ProductDetailListItem.Divider(1),
ProductDetailListItem.Rating(id = 2, rateCount = 5, upvoteRate = 60, downvoteRate = 40),
ProductDetailListItem.Divider(3),
ProductDetailListItem.NoneReview(id = 11),
ProductDetailListItem.Divider(3),
ProductDetailListItem.ReviewHeader(id = 4, reviewCount = 5),
ProductDetailListItem.Review(id = 5, nickname = "사랑합니다.", writeDate = "", isUpvote = false, reviewText = "고갱님", isLike = false, likeCount = 0),
ProductDetailListItem.Review(id = 6, nickname = "사랑합니다.", writeDate = "", isUpvote = true, reviewText = "고갱님", isLike = true, likeCount = 2),
ProductDetailListItem.Review(id = 7, nickname = "사랑합니다.", writeDate = "", isUpvote = true, reviewText = "고갱님", isLike = true, likeCount = 4),
ProductDetailListItem.Review(id = 8, nickname = "사랑합니다.", writeDate = "", isUpvote = false, reviewText = "고갱님", isLike = false, likeCount = 0),
ProductDetailListItem.Review(id = 9, nickname = "사랑합니다.", writeDate = "", isUpvote = true, reviewText = "고갱님", isLike = true, likeCount = 5),
ProductDetailListItem.Review(id = 10, nickname = "사랑합니다.", writeDate = "", isUpvote = false, reviewText = "고갱님", isLike = false, likeCount = 0)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.peonlee.feature.detail

import android.view.LayoutInflater
import android.view.View.generateViewId
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.doOnAttach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import coil.load
import com.peonlee.common.util.TimeUtil
import com.peonlee.core.ui.R
import com.peonlee.core.ui.adapter.MultiTypeListAdapter
import com.peonlee.core.ui.extensions.getStringWithArgs
import com.peonlee.core.ui.extensions.toFormattedMoney
import com.peonlee.core.ui.viewholder.CommonViewHolder
import com.peonlee.core.ui.viewholder.ViewOnlyViewHolder
import com.peonlee.feature.detail.databinding.ListItemDetailProductBinding
import com.peonlee.feature.detail.databinding.ListItemDividerBinding
import com.peonlee.feature.detail.databinding.ListItemEventBinding
import com.peonlee.feature.detail.databinding.ListItemNoneReviewBinding
import com.peonlee.feature.detail.databinding.ListItemRatingBinding
import com.peonlee.feature.detail.databinding.ListItemReviewBinding
import com.peonlee.feature.detail.databinding.ListItemReviewHeaderBinding
import java.time.LocalDateTime

class ProductDetailListAdapter : MultiTypeListAdapter<ProductDetailListItem, ProductDetailListItem.ViewType>() {
override fun onCreateViewHolder(viewType: ProductDetailListItem.ViewType, parent: ViewGroup): CommonViewHolder<ProductDetailListItem> {
return when (viewType) {
ProductDetailListItem.ViewType.PRODUCT -> ProductViewHolder(
ListItemDetailProductBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)

ProductDetailListItem.ViewType.RATING -> RatingViewHolder(ListItemRatingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
ProductDetailListItem.ViewType.REVIEW_HEADER -> ReviewHeaderViewHolder(
ListItemReviewHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)

ProductDetailListItem.ViewType.NONE_REVIEW -> NoneReviewViewHolder(
ListItemNoneReviewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)

ProductDetailListItem.ViewType.REVIEW -> ReviewViewHolder(ListItemReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false))
ProductDetailListItem.ViewType.DIVIDER -> DividerViewHolder(ListItemDividerBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
}

private inner class ProductViewHolder(private val binding: ListItemDetailProductBinding) : CommonViewHolder<ProductDetailListItem.Product>(binding) {
init {
binding.root.doOnAttach {
getItem { item ->
if (item.eventList.isEmpty()) {
binding.tvEventTitle.isGone = true
binding.flowEvent.isGone = true
return@getItem
}
item.eventList.forEachIndexed { index, event ->
val eventView = ListItemEventBinding.inflate(LayoutInflater.from(binding.root.context), binding.root, false).apply {
tvEventDes.text = event.title
ivStoreIcon.load(item.imageUrl)
root.id = generateViewId()
binding.root.addView(root, index)
}
binding.flowEvent.addView(eventView.root)
}
}
}
}

override fun onBindView(item: ProductDetailListItem.Product) = with(binding) {
ivProductImage.load(item.imageUrl)
tvProductName.text = item.productName
tvProductPrice.text = item.price.toFormattedMoney()
tvProductRecommended.text = getStringWithArgs(
R.string.item_product_recommended_percentage,
item.upvoteRate.toFloat()
)
tvReviewCount.text = getStringWithArgs(
R.string.item_product_review_count,
item.reviewCount
)
return@with
}
}

private inner class DividerViewHolder(binding: ListItemDividerBinding) : ViewOnlyViewHolder(binding)

private inner class ReviewHeaderViewHolder(private val binding: ListItemReviewHeaderBinding) :
CommonViewHolder<ProductDetailListItem.ReviewHeader>(binding) {
init {
binding.tvShowMoreButton.setOnClickListener {
getItem {
// TODO
}
}
}

override fun onBindView(item: ProductDetailListItem.ReviewHeader) = with(binding) {
tvReviewCount.text = getStringWithArgs(com.peonlee.feature.detail.R.string.count, item.reviewCount)
return@with
}
}

private inner class NoneReviewViewHolder(private val binding: ListItemNoneReviewBinding) : ViewOnlyViewHolder(binding) {
init {
binding.tvWriteReviewButton.setOnClickListener {
getItem {
// TODO
}
}
}
}

private inner class RatingViewHolder(private val binding: ListItemRatingBinding) : CommonViewHolder<ProductDetailListItem.Rating>(binding) {
override fun onBindView(item: ProductDetailListItem.Rating) = with(binding) {
tvTotalRateCount.text = getStringWithArgs(com.peonlee.feature.detail.R.string.rate_count, item.rateCount)
tvThumbsUpPercent.text = "${item.upvoteRate}%"
tvThumbsDownPercent.text = "${item.downvoteRate}%"

vThumbsUpRate.updateLayoutParams<LinearLayout.LayoutParams> {
weight = item.upvoteRate.toFloat()
}
vThumbsDownRate.updateLayoutParams<LinearLayout.LayoutParams> {
weight = item.downvoteRate.toFloat()
}
}
}

private inner class ReviewViewHolder(private val binding: ListItemReviewBinding) : CommonViewHolder<ProductDetailListItem.Review>(binding) {
override fun onBindView(item: ProductDetailListItem.Review) = with(binding) {
tvComment.text = item.reviewText
tvUserNameAndDate.text = getStringWithArgs(
R.string.item_recent_review_user_and_date,
item.nickname,
TimeUtil.getDuration(
itemView.context,
LocalDateTime.now()
)
)
tvLikeCount.text = item.likeCount.toString()
layoutThumbsDown.isVisible = item.isUpvote.not()
layoutThumbsUp.isVisible = item.isUpvote
return@with
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.peonlee.feature.detail

import com.peonlee.model.ListItem


sealed class ProductDetailListItem(override val viewType: ViewType) : ListItem {

enum class ViewType {
PRODUCT,
RATING,
REVIEW_HEADER,
NONE_REVIEW,
REVIEW,
DIVIDER
}

data class Product(
override val id: Long,
val imageUrl: String,
val productName: String,
val price: Int,
val upvoteRate: Int,
val reviewCount: Int,
val eventList: List<Event>
) : ProductDetailListItem(ViewType.PRODUCT)

//todo dummy domain model
data class Event(
val imageUrl: String,
val title: String
)

data class Rating(
override val id: Long,
val rateCount: Int,
val upvoteRate: Int,
val downvoteRate: Int,
) : ProductDetailListItem(ViewType.RATING)

data class ReviewHeader(
override val id: Long,
val reviewCount: Int
) : ProductDetailListItem(ViewType.REVIEW_HEADER)


data class NoneReview(
override val id: Long
) : ProductDetailListItem(ViewType.NONE_REVIEW)

data class Review(
override val id: Long,
val nickname: String,
val writeDate: String,
val isUpvote: Boolean,
val reviewText: String,
val isLike: Boolean,
val likeCount: Int
) : ProductDetailListItem(ViewType.REVIEW)

data class Divider(
override val id: Long
) : ProductDetailListItem(ViewType.DIVIDER)
}
Loading

0 comments on commit bb917fa

Please sign in to comment.