[Android / Kotlin] AppleMarket (4)

Subeen·2024년 1월 9일
0

Android

목록 보기
37/71

과제 목표

  • 좋아요 처리
    [v] 상품 상세 화면에서 좋아요 선택시 아이콘 변경 및 Snackbar 메세지 표시
    [v] 메인 화면으로 돌아오면 해당 상품에 좋아요 표시 및 좋아요 카운트 +1
    [v] 상세 화면에서 좋아요 해제시 이전 상태로 되돌림

결과 화면

MainActivity


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)
    }
}

DetailActivity

  • Snackbar 생성
Snackbar.make(
	binding.detailLayout, // 사용할 view의 id를 정의 해준다.
	getString(R.string.text_detail_snack_bar),
	Snackbar.LENGTH_SHORT
).show()

layout_detail

<?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
    }
}

ProductManager


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 }
}
profile
개발 공부 기록 🌱

0개의 댓글