안드로이드와 앱/프로젝트

개인 프로젝트 [에코마켓 앱]

정혜현 2024. 7. 11. 14:50

2024. 07. 11 시작!

Main page

조건 :

디자인 및 화면 구성을 최대한 동일하게 해주세요. (사이즈 및 여백도 최대한 맞춰주세요.) ✨
상품 데이터는 아래 dummy data 를 사용합니다. (더미 데이터는 자유롭게 추가 및 수정 가능)
RecyclerViewer를 이용해 리스트 화면을 만들어주세요.
상단 툴바를 제거하고 풀스크린 화면으로 세팅해주세요. (상태바(시간/배터리 표시하는 최상단바)는 남기고)
상품 이미지는 모서리를 라운드 처리해주세요.
상품 이름은 최대 두 줄이고, 그래도 넘어가면 뒷 부분에 …으로 처리해주세요.
뒤로가기(BACK)버튼 클릭시 종료하시겠습니까? [확인][취소] 다이얼로그를 띄워주세요. (예시 비디오 참고)
상단 종모양 아이콘을 누르면 Notification을 생성해 주세요. (예시 비디오 참고)
상품 가격은 1000단위로 콤마(,) 처리해주세요.
상품 아이템들 사이에 회색 라인을 추가해서 구분해주세요.
상품 선택시 아래 상품 상세 페이지로 이동합니다.
상품 상세페이지 이동시 intent로 객체를 전달합니다. (Parcelize 사용)

 

 

Detail page

조건 :

디자인 및 화면 구성을 최대한 동일하게 해주세요. (사이즈 및 여백도 최대한 맞춰주세요.) ✨
메인화면에서 전달받은 데이터로 판매자, 주소, 아이템, 글내용, 가격등을 화면에 표시합니다.
하단 가격표시 레이아웃을 제외하고 전체화면은 스크롤이 되어야합니다. (예시 비디오 참고)
상단 < 버튼을 누르면 상세 화면은 종료되고 메인화면으로 돌아갑니다.

 

 


 

 

 

viewBinding

 

그래들에 코드를 추가하고 사용한다. findViewById와 달리 입력한 id를 조금 다른 형태로 찾아내서 처음엔 적응이 안됐는데 타입을 안써도 되니 확실히 편하다.

 //gradle android{블럭 내 입력}
  viewBinding {
        enable = true
    }
//Activity onCreate내 입력
 _binding = ActivityDetailPageBinding.inflate(layoutInflater)
        setContentView(_binding.root)

 

_binding의 언더바는 private한 변수를 사용할 때 관례적으로 붙이는 특수기호라고 해서 써봤는데 가독성이 더 좋아져서 마음에 든다.  R.Drawble, R.String 등의 리소스의 id는 int타입이다. 뷰와 연결해줄 땐 타입에 주의

val dec = DecimalFormat("#,###원")

        holder.image.setImageResource(item[position].image)
        holder.title.text = item[position].title
        holder.location.text = item[position].location
        holder.price.text = dec.format(item[position].price)
        holder.chat.text = item[position].chat.toString()
        holder.like.text = item[position].like.toString()

 

data

 

지난 팀프로젝트에서 닮고 싶은 부분이 많아 이번 프로젝트에 적용해본 것들이 있었는데 그 중 하나가 string파일로 관리하는 것이었다. 그런데 1000단위 변환을 적용해보려고 하면 주소값이 나온다. 결국 튜터님 말씀따라 하드코딩으로 해결됐으나 아쉬웠다,, 원래는 큰따옴표 없이 작성된 글이지만 옮기느라 작성되어있다.

더보기

<!--  눈물나는 삽질...  -->
<!--    <string name="fan_title">산지 한달된 선풍기 팝니다"</string>-->
<!--    <string name="fan_introduce">"이사가서 필요가 없어졌어요 급하게 내놓습니다"</string>-->
<!--    <string name="fan_seller">"대현동"</string>-->
<!--    <string name="fan_price">"1000"</string>-->
<!--    <string name="fan_location">"서울 서대문구 창천동"</string>-->

<!--    <string name="kimchi_fridge_title">"김치냉장고"</string>-->
<!--    <string name="kimchi_fridge_introduce">"이사로인해 내놔요"</string>-->
<!--    <string name="kimchi_fridge_seller">"안마담"</string>-->
<!--    <string name="kimchi_fridge_price">"20000"</string>-->
<!--    <string name="kimchi_fridge_location">"인천 계양구 귤현동"</string>-->

<!--    <string name="chanel_wallet_title">"샤넬 카드지갑"</string>-->
<!--    <string name="chanel_wallet_introduce">"고퀄지갑이구요\n사용감이 있어서 싸게 내어둡니다"</string>-->
<!--    <string name="chanel_wallet_seller">"코코유"</string>-->
<!--    <string name="chanel_wallet_price">"10000"</string>-->
<!--    <string name="chanel_wallet_location">"수성구 범어동"</string>-->

<!--    <string name="safe_title">"금고"</string>-->
<!--    <string name="safe_introduce">"금고\n떼서 가져가야함\n대우월드마크센텀\n미국이주관계로 싸게 팝니다"</string>-->
<!--    <string name="safe_seller">"Nicole"</string>-->
<!--    <string name="safe_price">"10000"</string>-->
<!--    <string name="safe_location">"해운대구 우제2동"</string>-->

<!--    <string name="smart_phone_title">"갤럭시Z플립3 팝니다"</string>-->
<!--    <string name="smart_phone_introduce">"갤럭시 Z플립3 그린 팝니다\n항시 케이스 씌워서 썻고 필름 한장챙겨드립니다\n화면에 살짝 스크래치난거 말고 크게 이상은없습니다!"</string>-->
<!--    <string name="smart_phone_seller">"절명"</string>-->
<!--    <string name="smart_phone_price">"150000"</string>-->
<!--    <string name="smart_phone_location">"연제구 연산제8동"</string>-->

<!--    <string name="prada_bag_title">"프라다 복조리백"</string>-->
<!--    <string name="prada_bag_introduce">"까임 오염없고 상태 깨끗합니다\n정품여부모름"</string>-->
<!--    <string name="prada_bag_seller">"미니멀하게"</string>-->
<!--    <string name="prada_bag_price">"50000"</string>-->
<!--    <string name="prada_bag_location">"수원시 영통구 원천동"</string>-->


<!--    <string name="tiket_title">"울산 동해오션뷰 60평 복층 펜트하우스 1일 숙박권 펜션 힐링 숙소 별장"</string>-->
<!--    <string name="tiket_introduce">"울산 동해바다뷰 60평 복층 펜트하우스 1일 숙박권\n(에어컨이 없기에 낮은 가격으로 변경했으며 8월 초 가장 더운날 다녀가신 분 경우 시원했다고 잘 지내다 가셨습니다)\n1. 인원: 6명 기준입니다. 1인 10,000원 추가요금\n2. 장소: 북구 블루마시티, 32-33층\n3. 취사도구, 침구류, 세면도구, 드라이기 2개, 선풍기 4대 구비\n4. 예약방법: 예약금 50,000원 하시면 저희는 명함을 드리며 입실 오전 잔금 입금하시면 저희는 동.호수를 알려드리며 고객님은 예약자분 신분증 앞면 주민번호 뒷자리 가리시거나 지우시고 문자로 보내주시면 저희는 카드키를 우편함에 놓아 둡니다.\n5. 33층 옥상 야외 테라스 있음, 가스버너 있음\n6. 고기 굽기 가능\n7. 입실 오후 3시, 오전 11시 퇴실, 정리, 정돈 , 밸브 잠금 부탁드립니다.\n8. 층간소음 주의 부탁드립니다.\n9. 방3개, 화장실3개, 비데 3개\n10. 저희 집안이 쓰는 별장입니다."</string>-->
<!--    <string name="tiket_seller">"굿리치"</string>-->
<!--    <string name="tiket_price">"150000"</string>-->
<!--    <string name="tiket_location">"남구 옥동"</string>-->

<!--    <string name="chanel_bag_title">"샤넬 탑핸들 가방"</string>-->
<!--    <string name="chanel_bag_introduce">"샤넬 트랜디 CC 탑핸들 스몰 램스킨 블랙 금장 플랩백 !\n + "\n" + "색상 : 블랙\n" + "사이즈 : 25.5cm * 17.5cm * 8cm\n" + "구성 : 본품더스트\n" + "\n" + "급하게 돈이 필요해서 팝니다 ㅠ ㅠ"</string>-->
<!--    <string name="chanel_bag_seller">"난쉽"</string>-->
<!--    <string name="chanel_bag_price">"180000"</string>-->
<!--    <string name="chanel_bag_location">"동래구 온천제2동"</string>-->

<!--    <string name="spray_title">"4행정 엔진분무기 판매합니다."</string>-->
<!--    <string name="spray_introduce">"3년전에 사서 한번 사용하고 그대로 둔 상태입니다. 요즘 사용은 안해봤습니다. 그래서 저렴하게 내 놓습니다. 중고라 반품은 어렵습니다.\n"</string>-->
<!--    <string name="spray_seller">"알뜰한"</string>-->
<!--    <string name="spray_price">"30000"</string>-->
<!--    <string name="spray_location">"원주시 명륜2동"</string>-->

<!--    <string name="celine_bag_title">"셀린느 버킷 가방"</string>-->
<!--    <string name="celine_bag_introduce">"22년 신세계 대전 구매입니당\n + "셀린느 버킷백\n" + "구매해서 몇번사용했어요\n" + "까짐 스크래치 없습니다.\n" + "타지역에서 보내는거라 택배로 진행합니당!"</string>-->
<!--    <string name="celine_bag_seller">"똑태현"</string>-->
<!--    <string name="celine_bag_price">"190000"</string>-->
<!--    <string name="celine_bag_location">"중구 동화동"</string>-->

더미 데이터는 지난 팀프로젝트에서 닮고 싶은 또다른 부분이었던 데이터클래스와 오브젝트 클래스로 따로 빼서 관리했다.

더미 데이터 dummy data
유용한 데이터가 포함되지는 않지만, 공간을 예비해두어 실제 데이터가 명목상 존재하는 것처럼 다루는 정보
여러 건의 물리적 데이터를 이용하여 테스트 할 때 사용
소프트웨어에서 실제로는 사용되지 않아 제품 상에서는 볼 수 없지만 그 내부에만 존재하고 있는 데이터들을 의미

 

실제로는 유저의 데이터가 쓰이기 때문에 더미데이터가 적합하진 않지만 여기서 더미데이터의 의도는 단순히 MainActivity에서 리스트화로 관리하는 거라고 한다.

 

 

 

 

Recycler View

 

메인에서의 코드는 간단하다. 최상단에 어댑터를 선언하고 호출하면 된다.

  private fun getAdapter() {
        _binding.mainRecyclerView.adapter = adapter
        _binding.mainRecyclerView.layoutManager = LinearLayoutManager(this)
        val decoration = DividerItemDecoration(this,LinearLayoutManager.VERTICAL)
        _binding.mainRecyclerView.addItemDecoration(decoration)
    }

 

divider로 구분선을 쉽게 구현할 수 있다고 하는데 솔직히 쉽지 않았다. 왜냐하면 어느 문서든 작성 코드만 알려줄 뿐 작성 위치는 잘 안나와있었고 (this,VERTICAL)로만 나와있던 코드 때문에 한참을 돌아왔다.

어댑터는 따로 클래스를 만들어 recycler뷰와 뷰를 이루는 item을 실질적으로 관리하게 된다.

class Adapter(private val item: MutableList<ProductInfo>) : RecyclerView.Adapter<Adapter.Holder>() {

	//홀더 생성하는 함수
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.Holder {
        val binding =
            ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }
	//홀더에 뷰와 값을 연결하는 함수
    override fun onBindViewHolder(holder: Holder, position: Int) {
    holder.image.setImageResource(item[position].image)
    }
	//홀더 개수 정하는 함수
    override fun getItemCount(): Int {
        return item.size
    }
	//홀더 레이아웃 구성하는 함수
    inner class Holder(private val binding: ItemRecyclerviewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        val image = binding.ivItemTitle
    }
}

 

recycler 뷰를 쓰기위한 어댑터 클래스를 상속받으면 필수로 override해야하는 함수가 많다. 내가 뇌 상속받고 싶다고 한 팀원분께서 직접 만드신 자료도 주시고 코드 동작도 보여주셔서 동작원리를 이해하는데 큰 도움이 됐다.

 

나만보기 아까워서 허락받고 블로그에 업로드하기로 했다. 자료는 본문 수정중이라 완성되면 링크첨부.

 

 

 

intent

 

상품을 클릭하면 디테일 페이지로 이동해야되는데 안돼서 2시간을 헤맸다. 팀원에게 공유해보니 '엇 잠시만요 길게 눌러보시겠어요?' 라고 하셔서 꾹 눌렀더니 이동됐다. 알고보니 자동추천으로 뜬 Listener가 LongClickListener였다.

 holder.itemView.setOnClickListener {
            itemClick?.onClick(it, position)
        }

 

지난 팀프로젝트에서 닮고 싶은 또다른 부분이었던 함수로 코드 작성하기도 이번에 실천해봤다. 매번 onCreate내에 몰아서 쓰던 걸 분리시켜보니까 꽤 어려웠고 intent는 onCreate 내에서 해야한다는 것을 배웠다. intent 함수를 만들고 onCreate내에 호출시켜도 가능했다.

 

intent로 간단한 값만 받아올 방법을 찾아보니 startActivityForResult()라는 게 있었는데 deprecated 되었다. 그 대안으로 나온게 이미 써본 경험이 있는 registerForActivityResult()라서 반갑기도하고 신기하기도 했다. 코드 가독성, REQUEST_CODE 사용 제외, 타입 안정성 등이 개선되었다고 한다. 일단 지금은 intent로만 넘겼는데 필요시 변경하기로 했다. 

 

 

 

Dialog

 private fun btnBackListener() {
        val btnBackListener = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
            	//다이얼로그 빌더
                val builder = AlertDialog.Builder(this@MainPageActivity)
                builder.setTitle(getString(R.string.eco_market))//제목
                builder.setMessage(getString(R.string.ask_finish))//내용
                builder.setIcon(R.drawable.ic_logo_eco)//아이콘
				//버튼별 이벤트
                val btnListener = DialogInterface.OnClickListener { dialog, which ->
                    when (which) {
                        DialogInterface.BUTTON_POSITIVE -> finish()
                    }
                }
                builder.setPositiveButton(getString(R.string.yes), btnListener)
                builder.setNegativeButton(getString(R.string.no), btnListener)
                builder.show()
            }
        }
        onBackPressedDispatcher.addCallback(this, btnBackListener)
    }

 

지금까지는 버튼을 만들어서 뒤로가기 등의 이벤트는 해봤는데 뒤로가기를 눌렀을 때 이벤트 처리하는 건 처음이었다. 설정에서 네비게이션바가 보이도록 변경해야한다. 뒤로가기 버튼은 코드로 어떻게 입력하는지 찾아보니 함수를 작성하고 콜백으로 불러 사용한다. 

다이얼로그는 빌더를 만들고 버튼은 총 3가지가 있는데 각각의 이벤트를 설정해주면 된다. 

 

 

 

Notification

알림은 아무리 살펴봐도 문제가 없는데 안돼서 최후까지 미뤄놨는데 팀원분들과 공유해보니 앱 기본설정에 알림이 비허용되어있다는 걸 알게 됐다. 

private fun btnNotificationListener() {
        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 = "My Channel One"
            val channel = NotificationChannel(
                channelId,
                channelName,
                NotificationManager.IMPORTANCE_DEFAULT
            )
            //채널 등록
            manager.createNotificationChannel(channel)
            //채널을 이용하여 빌더 생성
            builder = NotificationCompat.Builder(this, channelId)
        } else {
            //버전 이하
            builder = NotificationCompat.Builder(this)
        }
        builder.run {
            setSmallIcon(R.drawable.ic_logo_eco)
            setWhen(System.currentTimeMillis())
            setContentTitle(getString(R.string.notifitacion_title))
            setContentText(getString(R.string.notifitacion_introduce))
        }
        manager.notify(1, builder.build())
    }

 

 

 

툴바는 따로 만들어서 참조했다.

<include
        android:id="@+id/toolbar"
        layout="@layout/toolbar_main_page" />

 

 

 

depth를 적게 하고자 웬만하면 레이아웃을 중첩되지 않게 작성하고 싶었는데 디테일페이지는 버튼이 이미지 위로 올라와야하고 스크롤링도 가능해야해서 어쩔 수 없이 ConstraintLayout [ ScrollView + ConstraintLayout + [FrameLayout] ] 구조가 됐다. 여기서 divider를 적용하고자 레이아웃을 또 나눌 수는 없겠어서 뷰로 만들었다. 한줄짜리 구분선은 오히려 이렇게 구성하는게 좋겠다.

 

 <View
                android:id="@+id/iv_detail_divider"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:layout_margin="10dp"
                android:background="@color/eco_green"
                app:layout_constraintStart_toStartOf="@+id/iv_detail_seller"
                app:layout_constraintTop_toBottomOf="@+id/tv_detail_eco_level" />

 

 

 

2024. 07. 17

수업듣고 개선된 코드로 바꾸었는데 시작하자마자 앱이 꺼졌다. 로그 자체가 안나오는 상태라 확인이 어려웠지만 코드를 하나하나 주석처리하면서 찾아본 결과, 어댑터 클래스에서 문제가 발생한다. 한꺼번에 수정을 많이해서 원인을 찾기가 힘들었는데 알고보니 메인에서 어댑터 정의가 되어있지 않은 상태로 호출해서 그렇다. 수정하면서 어댑터 선언부가 함수보다 아래로 가있었다. 선언부를 상단으로 올리니 해결됐다. 

 

LongClickListener는 Boolean형으로 반환해줘야한다. 

 

삭제해도 계속 데이터가 새로고침한 것처럼 그대로 생긴다. 원본 데이터에서 같이 제거해줘야하는데 그대로 남아있어서 그렇다. removeAt()으로 해당 position을 넘겨줘서 제거하니 해결됐다.

 

https://hungseong.tistory.com/24

 

 

 

 

2024. 07. 18

floating button

계속 검정색으로만 나온다.

app:tint="@color/white" 직접 다 쳐야된다.

app:rippleColor="@cl"클릭 이펙트 컬러

fabSize : 버튼 크기 normal/auto/mini

fabCustomSize : 버튼크기dp

maxImageSize : 내부 이미지 크기dp

foreground : 원하는 이미지 넣는 방법!!!이거다 대신 내부이미지 크기 조절이 안된다...

foregroundtint로 색깔도 바꿀 수 있다

 


 

회고

 

이번 프로젝트는 마감기한은 여유로우나 개인적인 사정으로 실제 마감보다 이틀이 부족하고, 다른 과제도 많아서 쫓기듯이 진행했다. 기능에 고민하기보다 어이없는 실수들로 낭비한 시간이 많아서 아쉬웠다. 

그래도 로그 찍는 법도 배워 활용해보고, 지난 프로젝트에서 배우고 싶었던 부분들을 복습해볼 수 있어 좋았고 새로 알게된 부분도 많아서 만족한다.

 

문제가 발생한다면 일단 onCreate로 옮겨 볼 것 : 아직 분리해서 작성하는 게 미숙하다. 위치의 문제인지 코드의 문제인지 확인해야하므로 옮겨서 원인을 찾아보자

자동완성주의 : 비슷한 이름의 함수, 메소드, 프로퍼티 등이 많다. 자동추천은 유용하나 내가 찾는 게 맞는지 확인하자

앱 기본기능을 생각해볼 것 : 알림설정, 뒤로가기버튼 등 생각보다 코드 외의 요소도 중요하다.

여러 사람의 의견 : 팀원과 공유해서 해결된 일이 많다. 다른 시야를 많이 자주 접해봐야겠다.