Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] #33-Product Detail UI 구현 #40

Merged
merged 1 commit into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
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,159 @@
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://coil-kt.github.io/coil/getting_started/
coil 라이브러리 사용시 Application class에서 ImageLoaderFactory를 구현 안해줘도 괜찮을까요~?.?
load()확장함수는 내부적으로

val imageLoader = imageView.context.imageLoader
val request = ImageRequest.Builder(imageView.context)
    .data("https://example.com/image.jpg")
    .target(imageView)
    .build()
imageLoader.enqueue(request)

위 코드를 실행하는데, ImageLoaderFactory 구현을 안하면 ImageLoader는 기본적으로 lazy하게 생성되는 것으로 알고있습니다!

제가 잘못 알고 있는 부분이 있다면 코멘트 남겨주시면 감사하겠습니답!!🙇‍🙇‍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lazy하게 생성되어도 괜찮을 것 같아요~!

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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오. 이 방법 배워갑니다. 👍🏻
CustomView만 생각하고 있었는데, 이런 방법이 있었네요.

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,61 @@
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)
}
28 changes: 28 additions & 0 deletions feature/detail/src/main/res/layout/activity_product_detail.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg0"
android:orientation="vertical">

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:padding="10dp"
android:src="@drawable/ic_close" />
</androidx.appcompat.widget.Toolbar>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvProductDetail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

</LinearLayout>
Loading