[TIL] Android 앱 개발 숙련 : 개인 과제 4 (보완)

지혜·2024년 1월 11일

Android_TIL

목록 보기
36/70

✏240111 목요일 TIL(Today I learned) 오늘 배운 것

코드 전문은 깃허브 참조! : Android_ch3_MyAppleMarket

📖코드에 적용한 보완 사항

  • 🚩키 값을 관리하기 위해 따로 파일 생성해서 상수값으로 저장하기

    • 실무에서는 키값의 경우 혼란을 주지 않고 공유할 수 있게 따로 파일을 만들어서 관리한다고 하셨다. 미리 경험해보는게 좋을 것 같아서 내 코드에도 반영해 보기로 했다.

      [Constants.kt]

      object Constants {
         const val DATA_POSITION = "dataPosition"
         const val PRODUCT_DATA = "productData"
         const val IS_DETAIL_LIKED = "isDetailLiked"
      }
      

      => Constants 코틀린 파일을 만들어서, const val로 상수를 선언해 준다. 이때, 상수의 이름은 대문자와 _(언더바)를 사용해서 만들어준다. 이렇게 만든 상수는 액티비티 코틀린 파일 안에서 Constants.DATA_POSITION으로 키 값에서 사용하면 된다.

      일단, 코드 내에서 키 값으로 사용하고 있던 것들을 전부 상수로 선언해줬는데, 이렇게 보니까 변수명을.. 이렇게 지었어도 됐나? 싶은 생각이 조금 든다. 한눈에.. 알아보기 어려우려나? 그냥 position을 매개변수 값으로 받아서 position을 썼는데.. index라고 쓰는게 더 직관적이였으려나 싶기도하고.. 그냥 모양이 낯설어서 그런건가 싶기도하고.. 어쨌든 상황과 회사에 맞춰서 작성하면 되는 부분이니까 지금은 중요하지 않을 것같아서 원래 사용하던 변수명 그대로 상수로 만들어서 코드 내에 걸어줬다.


  • 🚩좋아요를 체크하지 않았을 때, 상세페이지에 들어갔다 나오면 무조건 좋아요가 -1 되어있는 문제

    • 이건.. 보안사항이 아니고 걍 잘못 짠 코드를 수정한 것이다. 마침 해설영상에 똑같은 실수가 있었기 망정이지 아니였으면 틀렸는지도 몰랐을 뻔 헀다.

      [MainActivity.kt]

      //좋아요버튼 쌍방향통신 인텐트 콜백
       resultLaunchere = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
           if (result.resultCode == Activity.RESULT_OK) {
               val dataPosition = result.data?.getIntExtra(Constants.DATA_POSITION, 0) as Int
               val isDetailLiked = result.data?.getBooleanExtra(Constants.IS_DETAIL_LIKED, false) as Boolean
                     
               if (isDetailLiked) {
      //            [이전코드]          
      //             dataList[dataPosition].isLiked = true
      //             dataList[dataPosition].tvLiked += 1
      
                  //+현재 메인액티비티에서 해당하는 dataList 포지션의 isLiked가 false인지 판단
                   if (!dataList[dataPosition].isLiked) {
                       dataList[dataPosition].isLiked = true
                       dataList[dataPosition].tvLiked += 1
                   }
               } else {
      //            [이전코드]          
      //            dataList[dataPosition].isLiked = false
      //            dataList[dataPosition].tvLiked -= 1
      
                   //+현재 메인액티비티에서 해당하는 dataList 포지션의 isLiked가 true인지 판단
                   if (dataList[dataPosition].isLiked) {
                       dataList[dataPosition].isLiked = false
                       dataList[dataPosition].tvLiked -= 1
                   }
               }
      
               adapter.notifyItemChanged(dataPosition)
           }
       }

      => 상세페이지에서도 isLiked라고 쓰니까 아주 헷갈려서 MainDetailActivity.kt에서 사용하던 private var isLiked = false private var isDetailLiked = false로 바꿔주었다. 이렇게 바꾸니까 훨씬 이해가 잘간다. 처음 if문에서 상세페이지에서 넘어온 isDetailLiked가 true인지 false인지 확인하고, true라면 그 안에서 한 번 더 if문을 통해 메인페이지에서 현재의 isLiked값이 true인지 false인지 확인해서 false라면, isLiked값을 true로 바꿔주고 좋아요도 +1해주는 것이다. 만약 메인페이지에서 해당하는 포지션의 isLiked값이 이미 true라면 이중if문에서 걸려서 아무런 반응도 하지 않는다.

      변수명 지정을 어떻게 하느냐가 이렇게 중요하다는 것을 한 번 더 깨닫는다. 여기서도 isLiked 저기서도 isLiked하니까 다 똑같아 보여서 너무 어지러웠다. 진작 이렇게 할 껄. 그러면 어제 좋아요처리 트러블 슈팅도 금방 해결했을텐데..ㅎ 하는 생각이 든다.


  • 🚩Int형으로 선언한 tvPrice에 대해 format 맞춰주기

    • 원래 코드에서는 tvPrice를 String으로 선언하여 "10,000원"을 하드 코딩 해줬는데, 이번에 Int형으로 바꿔서 format을 맞춰주는 것으로 변경하기로 했다. 이번에는 딱히 필요없지만, 나중에 가격 등에 대해서 합산기능 등을 사용할 때는 int형으로 사용하는게 맞지 않을까? 하는 마음에 여기서부터 적용해보기로 했다.

      [ProductItem.kt, ProductAdapter.kt, MainDetailActivity.kt]

      //ProductItem.kt
       data class ProductItem(... , val tvPrice:Int, ...
       ) : Parcelable
      
       //ProductAdapter.kt
       @SuppressLint("SetTextI18n")
       override fun onBindViewHolder(holder: Holder, position: Int) {
       ...
       holder.tvPrice.text = DecimalFormat("#,###").format(productItem[position].tvPrice) + "원"
       ...
       }
      
       //MainDetailActivity.kt
       @SuppressLint("SetTextI18n")
       override fun onCreate(savedInstanceState: Bundle?) {
       ...
       binding.tvDetailPrice.text = DecimalFormat("#,###").format(productData?.tvPrice)
       ...
       }

      => 메인페이지의 리사이클러뷰 아이템에서도 포멧이 적용된 가격이 나올 수 있도록 holder를 맞춰주고, 상세페이지에서도 포맷이 적용된 가격이 나올 수 있도록 바인딩을 잡아준다.

      맨처음에 DecimalFormat("#,###").format(productData?.tvPrice)으로 바꾸니까 노란형광줄이 너무 시선을 강타했다. 보니까 @SuppressLint("SetTextI18n")를 달아야한다는데,, 검색해보니까 TextView에 할당할 text를 하드코딩하지 마라고 경고가 뜨는거라고 했다. 그래서 그럼 strings.xml에 넣어보려고 했는데, ("#,###)이랑 "원"을 넣었는데도 노란형광줄이 사라질 생각을 안해서 그냥 얌전히 어노테이션을 걸었다. 시간의 여유가 남는다면 한번더 strings.xml에 넣어보는 것을 도전해봐야겠다. 근데 함수들이랑 같이 쓰고 +로 연결해준거라서.. 애초에 안되지 않을까? 하고 생각해본다.


  • 🚩onScrollStateChanged 사용하기

    • 원래코드에서는 onScrolled를 사용했었다. onScrolled는 스크롤의 변화량을 감지하고, onScrollStateChanged는 스크롤 상태의 변경을 감지한다고 한다. 스크롤의 변화량 감지보다는 상태의 변경이 더 적합한 것 같아서 코드를 바꾸기로 했다.

      [MainActivity.kt]

      //플로팅버튼 스크롤상태에 따라 fadein/fadeout효과를 통해 표시하기.
            binding.productRecycle.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                    super.onScrollStateChanged(recyclerView, newState)
                    if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
                        if (binding.btnFloating.visibility != View.GONE) {
                            binding.btnFloating.apply {
                                startAnimation(AnimationUtils.loadAnimation(this@MainActivity, R.anim.fade_out))
                                visibility = View.GONE
                            }
                        }
                    } else {
                        if (binding.btnFloating.visibility != View.VISIBLE) {
                            binding.btnFloating.apply {
                                visibility = View.VISIBLE
                                startAnimation(AnimationUtils.loadAnimation(this@MainActivity, R.anim.fade_in))
                            }
                        }
                    }
                }
            })
      // [이전 코드]        
      //    //플로팅버튼 스크롤상태에 따라 fadein/fadeout효과를 통해 표시하기.
      //    binding.productRecycle.addOnScrollListener(object : RecyclerView.OnScrollListener() {
      //        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
      //            super.onScrolled(recyclerView, dx, dy)
      //            if (!recyclerView.canScrollVertically(-1)) {
      //                if (binding.btnFloating.visibility != View.GONE) {
      //                    binding.btnFloating.apply {
      //                        visibility = View.GONE
      //                        startAnimation(AnimationUtils.loadAnimation(this@MainActivity, R.anim.fade_out))
      //                    }
      //                }
      //
      //            } else {
      //                if (binding.btnFloating.visibility != View.VISIBLE) {
      //                    binding.btnFloating.apply {
      //                        visibility = View.VISIBLE
      //                        startAnimation(AnimationUtils.loadAnimation(this@MainActivity, R.anim.fade_in))
      //                    }
      //                }
      //
      //            }
      //        }
      //    })
      //
      //}

      => onScrolled를 onScrollStateChanged로 바꾸고 최상단 조건에 && newState == RecyclerView.SCROLL_STATE_IDLE를 추가해줬다. 그리고 onScrollStateChanged를 사용하면, activity_main.xml에서 플로팅버튼에 android:visibility="gone"를 추가해줘야 맨 처음 어플리케이션이 로드 될 때 버튼이 보이지 않을 수 있다. 해설영상에서 newState == RecyclerView.SCROLL_STATE_IDLE에 대한 조건을 보고 굳이 필요한가? 하고 없애봤는데, 이 조건이 없으면 맨 처음 최상단에서 스크롤을 내리는 중에는 버튼이 나타나지 않고, 스크롤을 내리고 난 후에야 버튼이 등장한다. 차이가 미묘해서 여러번 돌려보고나서야 이상한 점을 알아서 나중에 황급히 추가해줬다.ㅎ

      +나머지는 원래 사용했던 코드를 최대한 재활용했다. 해설 영상에서는 isTop 변수를 사용해서 구분해줬는데, if (binding.btnFloating.visibility != View.GONE)이 이미 잘되는 코드이기도하고, 보여질 때 사라지게하고 안보여질때 보이게 하는 것을 직관..적으로 느낄 수 있게 해주는 것 같기도해서 굳이 바꿀 필요까지는 없을 것 같다.


📖코드에 적용하지 않았지만 기억할 보완 사항

  • 🚩TextView 자체에 background를 걸어 버튼처럼 보이게 하기
    🚩작은 ImageView를 큰 영역의 레이아웃으로 감싸서 클릭 가능 영역 넓히기

    • 상세페이지에서 채팅하기 버튼을 그냥 TextView를 사용하셔서 만들었다. 거기에 background를 적용해서 둥근 모서리의 배경색을 지정한 xml을 연결할 수 있었다. 좋아요에 대한 ImageView도 크기가 너무 작으니까 겉에 리니어레이아웃으로 한 번 더 감싸 주어 클릭 가능한 영역을 넓혀주셨는데,
      => 내 코드에서는 채팅하기는 그냥 Button으로 구현했고, 좋아요 아이콘은 ImageView로 구현했지만, 아이콘의 위치를 잡아주는 과정에서 android:paddingEnd="16dp"를 줬더니 크기도 적절히 같이 조정이 되었기 때문에, 아이콘 크기는 작아도 클릭 가능 영역이 널널해서 문제가 없었다. 그리고 이 아이콘 이미지뷰에 회색선을 주기 위해서 ImageView에 background를 따로 걸어줬다. 원래는 가격TextVeiw쪽에 회색선 background를 줬다가 여백이 애매해서 ImageView쪽으로 걸어준거라, 알려주신 팁을 이미 사용해서 굳이 연습할 필요는 없을 것 같아서 굳이 코드를 바꾸지는 않았다.

  • 🚩xml파일을 거치지 않고 직접 fade in/out 애니메이션 코드 적용

    [MainActivity.kt]

    val fadeIn = AlphaAnimation(0f, 1f).apply { duration = 500 }
     val fadeOut = AlphaAnimation(1f, 0f).apply { duration = 500 }
     
     //플로팅버튼 스크롤상태에 따라 fadein/fadeout효과를 통해 표시하기.
     binding.productRecycle.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            ...
                if (!recyclerView.canScrollVertically(-1)) {
                   if (binding.btnFloating.visibility != View.GONE) {
                       binding.btnFloating.apply {
                           startAnimation(fadeOut)
                           visibility = View.GONE
                       }
                    }
                 }
                               ...
     })
    • 메인 액티비티 코틀린 파일에 바로 AlphaAnimation을 통해 바로 애니메이션을 만들고 startAnimation을 적용하는 방법이 있었다.
      => 이미 만들어둔 xml파일이 있어서, 굳이 이 방법을 적용..하지는 않았는데, 만약 알았더라면.. 이걸 적용했을거다.ㅎ 이게 더 간편해 보인다. 여러모로.

      +애니메이션에 대해 설명해주시면서 fade out이 먼저 나온 후 버튼이 사라지고, 버튼이 보여진 후 fade in이 시작되여야 버튼에 깜박임이 없다고 애니메이션이 어느 위치에 존재하는지도 중요하다고 해주셨다. 솔직히 솔직히 내코드는 GONE 다음에 fadeOut애니메이션이 왔는데, 전혀.. 이상을 못느꼈다가 얘기를 해주셔서 바꿔보니까 뭔가 더 부드러운 것 같아서..ㅎ 그렇구나 하고 위치를 바꿔주었다. 이번에는 별로 티가 안나서 잘 몰랐지만, 다른 애니메이션을 걸 때에는 주의 깊게 살펴보고 애니메이션을 적용할 수 있도록 신경써야겠다.

✏오늘의 느낀점

  • 해설 영상을 보여주시면서, 정답.코드가 아니고 보편적인 코드이니까 더 좋은 코드가 있다면 그 코드를 사용하는 것이 좋다고 해주셨다. 구분 방법은..ㅎㅎ 모르겠으면 튜터님께 물어보러 오라고 하셨는데, 나는 일단 내 임의대로 판단을 해보았다. 그래도 역시 대부분은 배우는 입장이다 보니까 부족한 점을 보여주신 영상으로 열심히 보완했던 것 같다.

  • 사실 이번 과제에는 저번 과제들의 피드백에 대해 반영하는 것을 의식하지 않고 그냥 작성헀는데, 뷰 바인딩 때문에 저절로 카멜케이스를 쓰고.. intent에서 데이터를 받아왔을 때 null 처리 안하면 오류 떠서 as로 지정된 타입 캐스트를 할 수 있도록 걸어주고.. 어느 정도 자동으로 반영된 피드백들도 있는 반면에, 기능별로 folder를 나누거나 반복되어 사용되는 값들을 상수로 선언하여 사용하면 좋다는 피드백은 애초에 기억도 잘 안났던 것 같다. 둘다 잘 안하던 버릇이여서, 나중에 더 크고 복잡한 프로젝트가 되면 얘네도 저절로 적용하게 될지.. 오늘 다시 한번 상기하면서 머리 속 한켠에 잘 정리해두어야겠다.

  • 오늘 보완수정하면서 느낀건데, data class를 만들 때, 안헷갈리려고 바인딩할 레이아웃 뷰들의 id를 따와서 변수명을 지었는데, dataList에서는 안그래도 됐을 것 같다는 생각이 조금 든다. isLiked도 그렇고...이름이.. 너무.. 똑같으면.. 오히려 헷갈릴 수 있다.ㅎ 그리고 데이터 연결할 때 tvPrice 이런식으로 하니까 오히려 이질감이 느껴졌다고 해야하나? 뷰바인딩을 통해 액티비티 코틀린 파일에 버튼이나 텍스트뷰 같은 거 이름이나 헷갈리지 않게 동일하게 만드는 정도만 하면 될 것 같다.

  • 하나 더 쓰잘데기 없는 고민을 하자면,, 이번 개인과제는 TIL작성을 할 때, 트러블슈팅에 관한 코드만 짤막하게 올려서 이번에 보완을 하는 과정에 다른 부분이 좀 있을 것 같은데 나중에 보면.. 헷갈리지 않을까..? 라는 생각에 다른 브런치로 올릴까 하다가.. 어짜피 안되는 코드 보면 더 노답일까봐 그냥 이 포스팅에 대충 백업해놓기로 하고 그냥 브런치를 추가하지 않고 main 브런치에다가 완성된 코드를 올리기로 했다.

  • 원래 오늘 수준별반 과제도 같이 할려고 했는데, 보완한다고 이것 저것 만지다 보니까 하루가 다 갔다. 내일이 금요일 평일이라서 얼마나 다행인지..ㅎ 차라리 이렇게 집중해서 개인과제를 정리하는 것이 좋을지도 모른다. 안 헷갈리고~ 내일은 수준별반 과제에 집중해보기로 한다!

profile
파이팅!

0개의 댓글