과제 목표
- 좋아요 처리
[v] 상품 상세 화면에서 좋아요 선택시 아이콘 변경 및 Snackbar 메세지 표시
[v] 메인 화면으로 돌아오면 해당 상품에 좋아요 표시 및 좋아요 카운트 +1
[v] 상세 화면에서 좋아요 해제시 이전 상태로 되돌림
class MainActivity : AppCompatActivity() {
companion object {
const val EXTRA_PRODUCT_ENTITY = "extra_product_entity"
const val NOTIFICATION_CHANNEL_ID = "one-channel"
const val NOTIFICATION_ID = 11
}
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private lateinit var productAdapter: ProductAdapter
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showBackPressedAlertDialog()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initView()
setActivityResultLauncher()
this.onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private fun initView() {
initRecyclerView()
setSpinnerLocation()
with(binding) {
ivNotification.setOnClickListener {
showNotification()
}
floatingButton.setOnClickListener {
binding.productRecyclerView.smoothScrollToPosition(0) // 최상단 이동
}
}
}
private fun setActivityResultLauncher() {
/*
registerForActivityResult 메소드를 사용해 ActivityLauncher 생성
DetailActivity에서 전달 한 데이터(상품 아이디, 좋아요 상태)
*/
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK) {
val productId = it.data?.getIntExtra(EXTRA_PRODUCT_ID, -1) ?: -1
val status = it.data?.getBooleanExtra(EXTRA_PREFERENCE_STATUS, false)
if (productId < 0 || status == null) {
return@registerForActivityResult
}
setPreferenceStatus(
productId,
status
)
val position = getIndexProductItem(productId)
if (position >= 0) {
productAdapter.notifyItemChanged(position)
}
}
}
}
private fun initRecyclerView() {
val items = loadList()
productAdapter = ProductAdapter(applicationContext, items)
binding.productRecyclerView.run {
layoutManager = LinearLayoutManager(this@MainActivity)
addItemDecoration(
DividerItemDecoration(
this@MainActivity,
LinearLayoutManager.VERTICAL
)
)
this.adapter = productAdapter
addOnScrollListener(createScrollListener())
}
productAdapter.itemClick = object : ProductAdapter.ItemClick {
override fun onItemClick(view: View, position: Int) {
val intent = Intent(this@MainActivity, DetailActivity::class.java)
val data = items[position]
intent.putExtra(EXTRA_PRODUCT_ENTITY, data)
activityResultLauncher.launch(intent)
}
override fun onItemLongLick(view: View, position: Int) {
showAlertDialog(
getString(R.string.dialog_remove_title),
getString(R.string.dialog_remove_message),
R.drawable.img_main_chat_16dp,
getString(R.string.dialog_button_positive),
{
if (removeProductItem(position)) { // 아이템 삭제가 완료 됐을 때
productAdapter.notifyItemRangeRemoved(position, items.size)
}
},
getString(R.string.dialog_button_negative)
)
}
}
}
private fun createScrollListener(): RecyclerView.OnScrollListener {
return object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
with(binding.floatingButton) {
if (!binding.productRecyclerView.canScrollVertically(-1)) {
animate().alpha(0f).duration = 200
visibility = GONE
} else {
animate().alpha(1f).duration = 200
visibility = VISIBLE
}
}
}
}
}
private fun showBackPressedAlertDialog() {
showAlertDialog(
getString(R.string.dialog_title),
getString(R.string.dialog_message),
R.drawable.img_main_chat_16dp,
getString(R.string.dialog_button_positive),
{ finish() },
getString(R.string.dialog_button_negative)
)
}
private fun showAlertDialog(
title: String,
message: String,
iconResId: Int,
positiveButtonText: String,
positiveAction: () -> Unit,
negativeButtonText: String,
negativeAction: (() -> Unit)? = null
) {
AlertDialog.Builder(this@MainActivity).apply {
setTitle(title)
setMessage(message)
setIcon(iconResId)
setPositiveButton(positiveButtonText) { _, _ -> positiveAction.invoke() }
setNegativeButton(negativeButtonText, null)
negativeAction?.let { setNegativeButton(negativeButtonText) { _, _ -> it.invoke() } }
}.show()
}
private fun setSpinnerLocation() {
binding.spinnerLocation.adapter = ArrayAdapter(
this@MainActivity,
android.R.layout.simple_spinner_dropdown_item,
listOf(
getString(R.string.location_0)
)
)
}
private fun showNotification() {
createNotificationChannel()
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).apply {
setSmallIcon(R.mipmap.ic_launcher)
setWhen(System.currentTimeMillis())
setContentTitle(getString(R.string.notification_title))
setContentText(getString(R.string.notification_message))
}
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.notify(NOTIFICATION_ID, builder.build())
}
private fun createNotificationChannel() {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = getString(R.string.notification_channel_description)
enableVibration(true)
}
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
channel.setSound(uri, audioAttributes)
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
Snackbar.make( binding.detailLayout, // 사용할 view의 id를 정의 해준다. getString(R.string.text_detail_snack_bar), Snackbar.LENGTH_SHORT ).show()
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/detailLayout" // snackbar를 위한 id 설정
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DetailActivity">
class DetailActivity : AppCompatActivity() {
companion object {
const val EXTRA_PRODUCT_ID = "extra_product_id"
const val EXTRA_PREFERENCE_STATUS = "extra_preference_status"
}
private val binding: ActivityDetailBinding by lazy {
ActivityDetailBinding.inflate(layoutInflater)
}
private val productEntity: ProductEntity? by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getParcelableExtra(
EXTRA_PRODUCT_ENTITY, ProductEntity::class.java
)
} else {
intent?.getParcelableExtra(
EXTRA_PRODUCT_ENTITY
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initView()
}
private fun initView() {
setImageViewStatus()
setUnderLineText()
setProductEntity()
with(binding) {
ivBackwards.setOnClickListener {
setResult()
}
ivDetailPreference.setOnClickListener {
it.isSelected = !it.isSelected
showSnackBar(it.isSelected)
}
}
}
private fun setResult() { // DetailActivity -> MainActivity
Intent().run {
putExtra(EXTRA_PRODUCT_ID, productEntity?.id) // 상품 아이디
putExtra(EXTRA_PREFERENCE_STATUS, binding.ivDetailPreference.isSelected) // 좋아요 버튼 상태
setResult(RESULT_OK, this)
} // run, with, let
if (isFinishing.not()) finish()
}
private fun showSnackBar(selected: Boolean) {
if (selected) {
Snackbar.make(
binding.detailLayout,
getString(R.string.text_detail_snack_bar),
Snackbar.LENGTH_SHORT
).show()
}
}
private fun setImageViewStatus() { // 하트 이미지
binding.ivDetailPreference.isSelected = productEntity?.preferenceStatus == true // 버튼 상태
}
private fun setUnderLineText() {
binding.tvMannersTemperature.paintFlags = Paint.UNDERLINE_TEXT_FLAG // 텍스트 밑줄
}
private fun setProductEntity() {
productEntity?.resId?.let { binding.ivDetailImage.setImageResource(it) }
binding.tvDetailName.text = productEntity?.name
binding.tvDetailExplain.text = productEntity?.explain
binding.tvDetailSeller.text = productEntity?.seller
binding.tvDatailPrice.text =
productEntity?.price?.decimalFormat() + getString(R.string.text_won)
binding.tvDetailLocation.text = productEntity?.location
}
}
object ProductManager {
private val items: MutableList<ProductEntity> = arrayListOf()
fun Context.loadList(): MutableList<ProductEntity> {
items.clear()
val assetManager = assets
val inputStream = assetManager.open("dummy_data.tsv")
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
bufferedReader.forEachLine {
val tokens = it.split("\t")
val resource = resources.getIdentifier(tokens[1], "drawable", packageName)
val post = ProductEntity(
tokens[0].toInt(),
resource,
tokens[2],
tokens[3].replace("\\n", "\n").replace(" + ", "").replace("\"", ""),
tokens[4],
tokens[5].toInt(),
tokens[6],
tokens[7].toInt(),
tokens[8].toInt()
)
items.add(post)
}
return items
}
fun removeProductItem(position: Int): Boolean =
try {
items.removeAt(position)
true
} catch (e: IndexOutOfBoundsException) {
false
}
fun setPreferenceStatus(productId: Int, status: Boolean) {
val position = getIndexProductItem(productId)
if (position >= 0) {
val item = items[position]
if (item.preferenceStatus != status) {
item.preference += if (status) 1 else -1
item.preferenceStatus = !item.preferenceStatus
}
}
}
fun getIndexProductItem(productId: Int): Int = items.indexOfFirst { it.id == productId }
}