[TIL] 231002 회고

서정한·2023년 10월 2일
0

내일배움캠프 7기

목록 보기
54/66

Intro

  • 저번주부터 시작해서 팀프로젝트와 개인공부를 병행중이다. 추석연휴가 껴있으나 연휴가길어 사실상 프로젝트에 사용할 기간자체는 얼마되지않는다는게 함정..!

팀 프로젝트

  • 내가 맡은 부분은 Bookmark페이지이다. 간단하게 외부에서 누른 좋아요버튼에따라 해당 item을 bookmark에 추가해주거나 삭제해주는 그런일들을 한다.
  • 그렇게 난이도가높은 기술이 아니므로 만드는것 자체는 그리 어렵지않았다. 아래는 내가 만든 결과이다.
/*
* 작성자: 서정한
* 내용: DetailPage에서 북마크로 지정한 item들을 보여주고
* 선택한 아이템을 삭제하는 기능이 있습니다.
* */
class BookmarkFragment : Fragment() {
    private var _binding: FragmentBookmarkBinding? = null
    private val binding get() = _binding!!
    private val adapter by lazy {
        BookmarkListAdapter(onItemChecked = { position, item ->
            updateItemChecked(position, item)
        })
    }
    private val viewModel: BookmarkViewModel by viewModels()

    private val sharedViewModel: MainSharedViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentBookmarkBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        initView()
        initViewModel()
    }

    private fun initView() = with(binding) {
        bookmarkRecyclerview.adapter = adapter
        // Edit
        bookmarkEditTextview.setOnClickListener {
            val type: BookmarkViewType = BookmarkViewType.Edit
            changeVisibleForType(type)
            updateBookmarkListType(type)
        }
        // Close
        bookmarkCloseButton.setOnClickListener {
            val type: BookmarkViewType = BookmarkViewType.Normal
            changeVisibleForType(type)
            updateBookmarkListType(type)
        }
        // Remove
        bookmarkRemoveButton.setOnClickListener {
            removeDialog(requireContext())
        }
    }

    private fun initViewModel() {
        with(viewModel) {
            list.observe(viewLifecycleOwner) { list ->
                adapter.submitList(list)
            }
        }

        with(sharedViewModel) {
            bookmarkEvent.observe(viewLifecycleOwner) { event ->
                when (event) {
                    is MainSharedEventForBookmark.BookmarkItemForAdd -> {
                        viewModel.addBookmarkItem(event.item)
                    }
                    is MainSharedEventForBookmark.BookmarkItemForRemove -> {
                        viewModel.removeSelectedBookmarkItem(event.item)
                    }
                }
            }

            homeEvent.observe(viewLifecycleOwner) {event ->
                when(event) {
                    is MainSharedEventForHome.UpdateHomeItem -> {
                        if(event.item.isBookmarked) {
                            viewModel.addBookmarkItem(event.item.toBookmarkModel())
                        } else {
                            viewModel.removeSelectedBookmarkItem(event.item.toBookmarkModel())
                        }
                    }
                }
            }
        }
    }

    // Edit Mode Selector. Edit버튼을 클릭할경우 EditMode로 View를 업데이트하여 보여줍니다.
    // Close 버튼을 클릭할경우 Edit모드가 종료되고 UI를 업데이트 합니다.
    private fun changeVisibleForType(type: BookmarkViewType) = with(binding) {
        when (type) {
            BookmarkViewType.Edit -> {
                bookmarkCloseButton.visibility = View.VISIBLE
                bookmarkRemoveButton.visibility = View.VISIBLE
                bookmarkEditTextview.visibility = View.INVISIBLE
            }

            BookmarkViewType.Normal -> {
                bookmarkEditTextview.visibility = View.VISIBLE
                bookmarkCloseButton.visibility = View.INVISIBLE
                bookmarkRemoveButton.visibility = View.INVISIBLE
            }
        }
    }

    // Edit모드 여부에 따라 item의 viewType 데이터를 변경해줍니다.
    private fun updateBookmarkListType(type: BookmarkViewType) {
        viewModel.updateBookmarkListType(type)
    }

    // 체크박스상태 업데이트
    private fun updateItemChecked(position: Int, item: BookmarkModel) {
        viewModel.updateItemChecked(position, item)
    }

    // 삭제버튼 Dialog
    private fun removeDialog(context: Context) {
        val current = viewModel.list.value.orEmpty().toMutableList()
        val removes = ArrayList<BookmarkModel>()
        removes.addAll(current.filter { it.isChecked })

        if (removes.isEmpty()) {
            return
        }

        val builder = AlertDialog.Builder(context)
        builder.setTitle("삭제")
        builder.setMessage("정말 삭제하시겠습니까?")
        builder.setIcon(R.mipmap.ic_launcher)

        // 버튼 클릭시에 무슨 작업을 할 것인가!
        val listener = DialogInterface.OnClickListener { _, p1 ->
            when (p1) {
                DialogInterface.BUTTON_POSITIVE -> {
                    for (i in removes.indices) {
                        Util.removeBookmarkItemForSharedPrefs(requireContext(), removes[i])
                        viewModel.removeSelectedBookmarkItem(removes[i])
                        sharedViewModel.updateHomeItems(
                            removes[i].copy(
                                isBookmarked = false
                            )
                        )
                    }
                }

                DialogInterface.BUTTON_NEGATIVE -> {}
            }
        }
        builder.setPositiveButton("확인", listener)
        builder.setNegativeButton("취소", listener)

        builder.show()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}
/*
* 작성자: 서정한
* 내용: RecyclerView Adapter.
* viewType에 따라 Holder or EditHolder를 띄워줌.
* */
class BookmarkListAdapter(private val onItemChecked: (Int, BookmarkModel) -> Unit) :
    ListAdapter<BookmarkModel, RecyclerView.ViewHolder>(
        object : DiffUtil.ItemCallback<BookmarkModel>() {
            override fun areItemsTheSame(oldItem: BookmarkModel, newItem: BookmarkModel): Boolean {
                return oldItem.imgUrl == newItem.imgUrl
            }

            override fun areContentsTheSame(
                oldItem: BookmarkModel,
                newItem: BookmarkModel
            ): Boolean {
                return oldItem == newItem
            }
        }
    ) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            BookmarkViewType.Normal.INT -> {
                val view = BookmarkRecyclerItemBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                Holder(view)
            }

            BookmarkViewType.Edit.INT -> {
                val view = BookmarkRecyclerItemCheckboxBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                EditHolder(view, onItemChecked)
            }

            else -> {
                val view = BookmarkRecyclerItemBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                Holder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is Holder -> {
                holder.bind(getItem(position))
            }

            is EditHolder -> {
                holder.bind(getItem(position))
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position).viewType) {
            BookmarkViewType.Edit -> {
                BookmarkViewType.Edit.INT
            }

            BookmarkViewType.Normal -> {
                BookmarkViewType.Normal.INT
            }
        }

    }

    class Holder(private val binding: BookmarkRecyclerItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: BookmarkModel) = with(binding) {
            Glide.with(itemView.context)
                .load(item.imgUrl)
                .into(bookmarkItemImage)
            bookmarkItemTitle.text = item.title
            bookmarkItemContent.text = item.description
        }
    }

    class EditHolder(
        private val binding: BookmarkRecyclerItemCheckboxBinding,
        private val onItemChecked: (Int, BookmarkModel) -> Unit
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: BookmarkModel) = with(binding) {
            Glide.with(itemView.context)
                .load(item.imgUrl)
                .into(bookmarkItemImage)
            bookmarkItemTitle.text = item.title
            bookmarkItemContent.text = item.description
            bookmarkItemCheckbox.isChecked = item.isChecked

            // 체크박스 체크되어있을때만 ClickEvent 동작
            bookmarkItemCheckbox.setOnClickListener {
                onItemChecked(adapterPosition, item.copy(
                    isChecked = bookmarkItemCheckbox.isChecked
                ))
            }
        }
    }
}
/*
* 작성자: 서정한
* 내용: RecyclerView Adapter에서 ViewType 분기처리를 위한 sealed Class
* */
@Parcelize
sealed class BookmarkViewType : Parcelable {
    data object Normal : BookmarkViewType() {
        // adapter의 onCreateViewHolder에서 viewType 분기처리시 사용
        const val INT = 0
    }
    data object Edit: BookmarkViewType() {
        // adapter의 onCreateViewHolder에서 viewType 분기처리시 사용
        const val INT= 1
    }
  • 간단히 내용설명을 좀 하면 디즈니 앱에보면 Edit버튼이 있고 이것을누르면 view에 보이는 item의 타입이 바뀌는 형식이다. 아래 사진을 보면 한번에 이해될것이다.

  • 해당 내용을 구현하기위해 ViewType을 두가지로 만들었고, ViewType을 관리하기위해 sealed class를 사용하였다.
  • 구현하면서 딱히 어렵거나 힘든것은 없었으나 같이 팀으로 작업하는 상황이라 중간중간 조율하는 시간등이 소요되었는데 이것은 팀으로 개발하다보니 당연히 들어가는 시간이라는 생각이 들었다.

MVVM 공부 1회차!?!

  • 요즘 계속 MVVM공부중이다. 현재 domain영역과 usecase를 사용하는 것 그리고 DataSource에서부터 presentation에 이르기까지의 구조를 물흐르듯 이해하지 못하고있는 상황이라 구현하는 내내 버벅거렸다.
  • 보통 이럴때 머리에 쥐가나며 맨붕이 오는데, 일단 꾸역꾸역 진도를 나가 어떻게 usecase까지 다 연결해서 기어이 구현을 해버렸다..! 그러나 실행되지않고 오류가 발생하였는데 그 오류는 바로 http400번 오류였다!

    보통 400번 오류는 클라이언트에 서버가 정보를 주려고했는데 클라이언트쪽에 오류가발생해 정보를 받을 수 없을경우 발생하는 오류라고 한다.

  • 덕분에 retrofit에 okHttp의 log를 이전보다는 좀 더 자세히 살펴볼 기회가 생겼다! 그래서 당장 적용하는데 적용은 아래와같이했다. 생각보다 간단했는데 로그레벨 조정하여 인스턴스를 선언한 후 인터셉터에 집어넣어 패킷이 오가는 과정을 지켜보는 식인것 같다.
private val logging by lazy {
        HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }
    }

    private val okHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(AuthorizationInterceptor())
            .addInterceptor(logging)
            .build()
    }

    private val retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create(getDateFormatGsonBuilder()))
            .build()
    }
  • 로그를 찍어보니 잘들어왔다..! DataType이 잘못되었다고 생각해서 한참을 살펴봐도 보이지않았는데 결국 'document'를 발견해버렸다!!!!!(나는 daum 이미지, 동영상 검색 api를 사용했는데 서버에서 response로 보내주는 key 값은 'documents'이다.)
  • 한참 찾으면서 domain 영역과 data영역을 오르락내리락하다보니 눈에 조금 익었달까? 그래서 울렁증은 덜하지만 여전히 구현은 못하는 상태라 2번째 새로운 프로젝트를 파서 처음부터 다시 만드는 중이다.

Outro

  • 팀프로젝트를 진행하며 느끼는것은 리펙토링 각을 언제 잡을것인가? 그리고 이제 슬슬 속도를 내야할때인데 어느정도 속도감을 가지고 프로젝트를 대해야할까였다.
  • 원래는 완전히 MVVM패턴으로 리펙토링도 진행하고싶었지만 아무래도 시간상 그것은 무리일듯하다. 그렇다면 지금 프로젝트에서 코드라도 좀 이쁘게 정리해야겠다..!!!
profile
잘부탁드립니다!

0개의 댓글