티스토리 뷰

앱개발 숙련 과제

 

필수 과제 구현

 

구현 사항

// 메인 페이지 구현 사항
- 상품 데이터는 아래 dummy data 를 사용합니다. (더미 데이터는 자유롭게 추가 및 수정 가능)
- 뒤로가기(BACK)버튼 클릭시 종료하시겠습니까? [확인][취소] 다이얼로그를 띄워주세요.
- 상단 종모양 아이콘을 누르면 Notification을 생성해 주세요.
- 상품 가격은 1000단위로 콤마(,) 처리해주세요.
- 상품 선택시 아래 상품 상세 페이지로 이동합니다.
- 상품 상세페이지 이동시 intent로 객체를 전달합니다. (Parcelize 사용)

// 상세페이지 구현 사항
- 메인화면에서 전달받은 데이터로 판매자, 주소, 아이템, 글내용, 가격등을 화면에 표시합니다.
- 상단 < 버튼을 누르면 상세 화면은 종료되고 메인화면으로 돌아갑니다.

 

데이터 클래스 설계

// build.gradle.kts(:app)

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-parcelize")
}

Parcelize를 사용하기 플러그인을 위와 같이 설정해준다. 

 

@Parcelize
data class Item(
    val Image: Int,
    val ItemTitle: String,
    val ItemExplain: String,
    val SellerName: String,
    val Price: Int,
    val Address: String,
    var LikeCnt: Int,
    val CommentCnt: Int,
) : Parcelable

판매 상품의 정보를 출력하기 위해 해당하는 데이터 클래스를 설계해준다.

Parcelize를 사용해 객체를 직렬화 시킨 뒤 넘겨줄 수 있다.

 

 

더미 데이터 추가

private val dataList = mutableListOf<Item>()

제네릭 타입을 Item으로 가지는 가변형 리스트를 하나 선언해준다.

 

private fun initData(){
    dataList.add(Item(R.drawable.item1, "산진 한달된 선풍기 팝니다","이사가서 필요가 없어졌어요 급하게 내놓습니다", "대현동",1000, "서울 서대문구 창천동", 13, 25))
    dataList.add(Item(R.drawable.item2, "김치냉장고", "이사로인해 내놔요","안마담", 20000, "인천 계양구 귤현동", 8, 28))
    dataList.add(Item(R.drawable.item3, "샤넬 카드지갑", "고퀄지갑이구요\n사용감이 있어서 싸게 내어둡니다","코코유", 10000, "수성구 범어동", 23, 5))
    dataList.add(Item(R.drawable.item4, "금고", "금고\n떼서 가져가야함\n대우월드마크센텀\n미국이주관계로 싸게 팝니다","Nicole", 10000, "해운대구 우제2동", 14, 17))
    dataList.add(Item(R.drawable.item5, "갤럭시Z플립3 팝니다", "갤럭시 Z플립3 그린 팝니다\n항시 케이스 씌워서 썻고 필름 한장챙겨드립니다\n화면에 살짝 스크래치난거 말고 크게 이상은없습니다!","절명", 150000, "연제구 연산제8동", 22, 9))
}

데이터리스트를 더미 데이터들로 초기화한다.

 

 

키 네임 빼두기

object Constants {
    const val ITEM_INDEX = "item_index"
    const val ITEM_OBJECT = "item_object"
}

키 네임을 잘못 작성하여 데이터를 불러오지 못하는 사태를 방지하기 위해

오브젝트 파일에 키 네임을 따로 빼두는 것이 좋다.

 

 

어댑터 클래스 정의

// 어댑터 클래스는 아이템 리스트를 인자로 받고, 리싸이클러뷰의 어댑터 클래스를 상속함
class ItemAdapter(private val mItems: MutableList<Item>) : RecyclerView.Adapter<ItemAdapter.Holder>() {
	
    // 인터페이스를 통해 메인 액티비티로 클릭 이벤트를 넘겨줌
    interface ItemClick {
        fun onClick(view : View, position : Int)
    }
    
    // itemClick 변수는 ItemClick 인터페이스를 상속하며 메인에서 구현됨
    var itemClick : ItemClick? = null
	
    // 뷰홀더 생성
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }
	
    // 각 아이템에 데이터 바인딩
    override fun onBindViewHolder(holder: Holder, position: Int) {
		
        // 아이템 뷰에 클릭리스너 설정
        holder.itemView.setOnClickListener {
            itemClick?.onClick(it, position)
        }
		
        // 홀더의 각 뷰에다 포지션에 해당하는 데이터 대입
        holder.ivItemImg.setImageResource(mItems[position].Image)
        holder.tvItemTitle.text = mItems[position].ItemTitle
        holder.tvAddress.text = mItems[position].Address
		
        // 상품 가격은 1000단위로 콤마처리
        val price = mItems[position].Price
        holder.tvPrice.text = DecimalFormat("#,###").format(price)+"원"
		
        // 텍스트에는 String값을 대입해야함
        holder.tvCommentCnt.text = mItems[position].CommentCnt.toString()
        holder.tvLikeCnt.text = mItems[position].LikeCnt.toString()
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemCount(): Int {
        return mItems.size
    }
	
    // 홀더 클래스 내에는 아이템의 구성요소가 변수로 초기화됨
    inner class Holder(binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
        val ivItemImg = binding.ivItemImg
        val tvItemTitle = binding.tvItemTitle
        val tvAddress = binding.tvAddress
        val tvPrice = binding.tvPrice
        val tvCommentCnt = binding.tvCommentCnt
        val tvLikeCnt = binding.tvLikeCnt
    }
}

리싸이클러뷰를 쓰기 위해 어댑터 클래스를 정의한다.

 

 

어댑터 설정

private fun setAdapter(){
    val adapter = ItemAdapter(dataList)
    binding.recyclerView.adapter = adapter
    binding.recyclerView.layoutManager = LinearLayoutManager(this)

    adapter.itemClick = object : ItemAdapter.ItemClick {
        override fun onClick(view: View, position: Int) {
            val intent = Intent(this@MainActivity, DetailAcitivity::class.java)
            intent.putExtra(Constants.ITEM_INDEX, position);
            intent.putExtra(Constants.ITEM_OBJECT, dataList[position])
            startActivity(intent)
        }
    }
}

메인에서 리싸이클러뷰의 어댑터를 만들어둔 어댑터 클래스 객체로 초기화한다.

레이아웃 매니저를 LinearLayoutManager로 설정해 항목을 수직으로 배치시킨다.

 

ItemClick 인터페이스를 구현하여 특정 항목을 눌렀을 때의 동작을 정의한다.

인텐트를 통해 현재 항목의 Item클래스와 위치(Int)를 디테일 화면으로 넘겨준다. 

 

 

아이템 데이터 받아오기

private val item: Item? by lazy {
    // 안드로이드 버전이 13이상인지 확인
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        intent.getParcelableExtra(Constants.ITEM_OBJECT, Item::class.java)
    } else {
        intent.getParcelableExtra<Item>(Constants.ITEM_OBJECT)
    }
}

안드로이드 버전이 13이상이라면 직렬화된 객체를 가져오는 메소드의

두번째 인자값에 아이템 클래스를 할당해줘야한다.

 

버전에 따라 다른 방식으로 가져오기에 by lazy 키워드를 사용해

item 변수가 처음 사용되는 시점에서 item을 초기화 한다.

 

 

상세 페이지 데이터 바인딩

private fun onBind(){
    binding.ivItemImage.setImageDrawable(item?.let {
        ResourcesCompat.getDrawable(
            resources,
            it.Image,
            null
        )
    })

    binding.tvSellerName.text = item?.SellerName
    binding.tvSellerAddress.text = item?.Address
    binding.tvItemTitle.text = item?.ItemTitle
    binding.tvItemExplain.text = item?.ItemExplain
    binding.tvItemPrice.text = DecimalFormat("#,###").format(item?.Price) + "원"
}

상세 페이지의 구성요소에 해당하는 데이터를 바인딩하는 함수이다.

item 오브젝트의 데이터를 가져올 때는 null 처리를 필수적으로 진행해줘야한다.

 

setImageResource는 Int형 값을 인자로 넣어야 하기에

이미지뷰를 초기화할 땐 setImageDrawable을 이용하여 초기화 해줘야 한다.

 

어댑터에서 설정했던 것과 마찬가지로 상품 가격은 콤마 처리를 진행한다.

 

 

상세페이지 뒤로가기 버튼

binding.ivBack.setOnClickListener {
    finish()
}

상단 < 버튼을 눌렀을 때 현재 액티비티를 종료시켜 메인 액티비티로 돌아가도록 한다.

 

 

뒤로가기 버튼 클릭시 다이얼로그 띄우기

private val callback = object : OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
        val dialog = AlertDialog.Builder(this@MainActivity)
        dialog.setIcon(R.drawable.chat)
        dialog.setTitle("종료")
        dialog.setMessage("정말 종료하시겠습니까?")

        dialog.setPositiveButton("확인") { dialog, _ ->
            finish()
        }
        dialog.setNegativeButton("취소") { dialog, _ ->
            dialog.dismiss()
        }
        dialog.show()
    }
}

OnBackPressedCallback 객체를 생성해

뒤로 가기 버튼이 눌렸을 때 수행할 작업을 정의한다.

 

this.onBackPressedDispatcher.addCallback(this, callback)

onCraete에서 생성한 콜백을 OnBackPressedDispatcher에 추가한다.

 

 

Notification 생성

private fun checkNotiPermission(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) {
            val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
                putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
            }
            startActivity(intent)
        }
    }
}

사용자에게 알림 권한을 요청하는 함수로 알림 권한이 없다면 권한 설정 페이지로 이동한다.

 

 

private fun notification() {
    val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
    val builder: NotificationCompat.Builder
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val channelId = "one-channel"
        val channelName = "Alarm"
        val channel = NotificationChannel(
            channelId,
            channelName,
            NotificationManager.IMPORTANCE_DEFAULT
        ).apply {
            description = "Alarm Channel"
            setShowBadge(true)
            val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
            val audioAttributes = AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_ALARM)
                .build()
            setSound(uri, audioAttributes)
            enableVibration(true)
        }
        manager.createNotificationChannel(channel)
        builder = NotificationCompat.Builder(this, channelId)
    } else {
        builder = NotificationCompat.Builder(this)
    }
    builder.run {
        setSmallIcon(R.mipmap.ic_launcher)
        setWhen(System.currentTimeMillis())
        setContentTitle("키워드 알림")
        setContentText("설정한 키워드에 대한 알림이 도착했습니다!!")
    }
    manager.notify(1, builder.build())
}

안드로이드 버전을 확인한 뒤 채널을 생성하여 푸쉬 알림을 수신하는 함수이다.

 

private fun pushNotification(){
    binding.ivNotification.setOnClickListener{
        checkNotiPermission()
        notification()
    }
}

알림 버튼을 누르면 알림권환을 확인한 뒤 푸쉬알림을 수신한다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함