[TIL] 231004 회고

서정한·2023년 10월 4일
0

내일배움캠프 7기

목록 보기
55/66

Intro

  • 오늘 팀프로젝트의 구현마무리시간을 가졌다. 주요 구현내용은 ViewPager2를 사용하는 Fragment에서 ViewPager2에 사용되지않은 Fragment에 어떻게 넘어갈까였다.

Home or Search item 클릭시 Detail 페이지로 이동

  • 해당기능을 구현하기위해서는 Activity를 거쳐서 데이터를 전달해줘야한다. 그러니까 다음과 같이 전달된다. "HomeFragment" ->"DetailActivity" -> "DetailFragment"
  • 위와같이 구현해야하는 이유는 우리 프로젝트에서는 ViewPager2+TabLayout을 사용중인데, ViewPager2에 등록되지않은 새로운 Fragment를 화면에 띄우려면
    1) 기존 Fragment의 자식 Fragment로 설정해서 클릭이벤트시 띄운다
    2) DetailActivity를 만들고 그 위에 Fragment를 띄운다. 즉 DetailActivity를 띄우면 DetailActivity에서 DetailFragment로 Bundle 데이터와 함께 Fragment를 불러온다. 그렇게 작업한 결과가 아래 코드이다.
class DetailActivity : AppCompatActivity() {
    companion object {
        const val EXTRA_DETAIL = "extra_detail"
        fun newIntent(
            context: Context,
            item: DetailModel,
            ) : Intent = Intent(context, DetailActivity::class.java).apply {
            putExtra(EXTRA_DETAIL, item)
        }
    }

    private val detailBundle by lazy {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra(EXTRA_DETAIL, DetailModel::class.java)
        }else {
            intent.getParcelableExtra(EXTRA_DETAIL)
        }
    }

    private val binding by lazy {
        ActivityDetailBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        initBundle()
    }

    private fun initBundle() {
        detailBundle?.let {
            supportFragmentManager.beginTransaction().replace(
                R.id.detail_container,
                DetailFragment.newInstance(it),
            ).commit()
        }
    }

    // 뒤로가기 막기
    override fun onBackPressed() {
//        super.onBackPressed()
    }
}
class DetailFragment : Fragment() {
    companion object {
        const val BUNDLE_DETAIL = "bundle_detail"

        fun newInstance(item: DetailModel): DetailFragment = DetailFragment().apply {
            arguments = Bundle().apply {
                putParcelable(BUNDLE_DETAIL, item)
            }
        }
    }

    private var _binding: FragmentDetailBinding? = null
    private val binding get() = _binding!!

    private val viewModel: DetailViewModel by viewModels { DetailViewModelFactory() }
    private val sharedViewModel: MainSharedViewModel by activityViewModels()

    private val searchBundle by lazy {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            arguments?.getParcelable(BUNDLE_DETAIL, DetailModel::class.java)
        } else {
            arguments?.getParcelable(BUNDLE_DETAIL)
        }
    }

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

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

        initView()
        initViewModel()
    }

    /*
     * 
     * viewModel 상의 LiveData의 변경을 관찰해 값이 변하는 경우 Event 처리
     */
    private fun initViewModel() {

        with(viewModel) {
            // viewModel의 DetailItem이 변했을 경우
            detailItem.observe(viewLifecycleOwner, Observer {
                // bookmarkItem Update
                sharedViewModel.updateBookmarkItems(it)
                // homeItem Update
                sharedViewModel.updateHomeItems(viewModel.detailItem)
                onBind(it)
            })
        }
        with(sharedViewModel) {
            // sharedViewModel의 데이터가 변경이 일어나면 viewModel의 DetailItem 설정
            detailEvent.observe(viewLifecycleOwner, Observer { event ->
                when (event) {
                    is MainSharedEventForDetail.UpdateDetailItem -> {
                        updateItem(event.item)
                    }

                    else -> Unit
                }
            })
        }
    }

    private fun initView() = with(binding) {
        toolbar.setNavigationOnClickListener {
            searchBundle?.let { item ->
                val intent = Intent().apply {
                    putExtra(
                        DetailActivity.EXTRA_DETAIL,
                        item.copy(
                            isBookmarked = viewModel.getItemBookmarked()
                        )
                    )
                }
                requireActivity().setResult(Activity.RESULT_OK, intent)
                requireActivity().finish()
            }
        }

        detailPlayBtn.setOnClickListener {
            detailScrollview.smoothScrollTo(0, detailScrollview.bottom)// 하단 스크롤
        }

        detailBookmarkBtn.setOnClickListener {
            // isbookmarked btn Update
            detailBookmarkBtn.isSelected = !detailBookmarkBtn.isSelected
            // detailItem Update
            searchBundle?.let { item ->
                viewModel.isBookmarkedItem(
                    item.copy(
                        isBookmarked = detailBookmarkBtn.isSelected
                    )
                )
            }
        }

        detailShareBtn.setOnClickListener {
            /* 
             * context를 넣어줄 때 널체크를 해줌으로써 안정성을 높여줌
             * requireContext를 사용했을 땐 context가 null일 경우 IllegalStateException 발생
             */
            activity?.let { context ->
                viewModel.detailItem.value?.let { detailItem ->
                    Util.shareUrl(context, detailItem.imgUrl)
                }
            }
        }

        searchBundle?.let {
            viewModel.addDetailItem(it)
        }
    }

    /*
     * 
     * 디테일 item update
     */
    private fun updateItem(item: DetailModel) {
        viewModel.updateDetailItem(item)
    }

    private fun onBind(item: DetailModel) = with(binding) {
        Glide.with(requireContext())
            .load(item.imgUrl)
            .into(detailImageImageview)
        detailTitle.text = item.title
        detailDatetimeTv.text = item.datetime
        detailPlayerTitle.text = item.title
        detailPlayerDescription.text = item.description
        item.id?.let { setPlayer(it) }
    }

    /*
     * 
     * Youtube Video Player Library 사용
     * https://github.com/PierfrancescoSoffritti/android-youtube-player
     */
    private fun setPlayer(videoId: String) = with(binding) {
        detailPlayer.addYouTubePlayerListener(object :
            AbstractYouTubePlayerListener() {
            override fun onReady(youTubePlayer: YouTubePlayer) {
                super.onReady(youTubePlayer)
                // https://www.youtube.com/watch?v=KhEAe2_T-4c&t=4652s 링크 -> ID값 [KhEAe2_T-4c]
                // https://www.youtube.com/watch?v=42fmMP81EvA&t=2296s -> 42fmMP81EvA
                // 나중에 변환해주는 함수 추가 예정
                youTubePlayer.cueVideo(videoId, 0f)
            }
        })
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}
  • 동작은 위 gif와 같이 된다. 구현하면서 내가 놓쳤던부분은 Intent에 데이터를 실어보낸 이후 callback을 받아서 처리해야하는데 그렇게하지않고 새로 Activity를 Intent하게되어 SharedViewModel 데이터가 싹 날라갔었다 허헣... 그래서 아무리 북마크를 쌓아도 한개만 쌓이는 기적을 경험하였다. 해결한 코드는 아래에..!
// HomeFragment.kt

    private val detailLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                val item = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    result.data?.getParcelableExtra(
                        DetailActivity.EXTRA_DETAIL,
                        DetailModel::class.java
                    )
                } else {
                    result.data?.getParcelableExtra(DetailActivity.EXTRA_DETAIL)
                }

                sharedViewModel.updateHomeItems(item)
            }
        }
        
// DetailFragment.kt
toolbar.setNavigationOnClickListener {
            searchBundle?.let { item ->
                val intent = Intent().apply {
                    putExtra(
                        DetailActivity.EXTRA_DETAIL,
                        item.copy(
                            isBookmarked = viewModel.getItemBookmarked()
                        )
                    )
                }
                requireActivity().setResult(Activity.RESULT_OK, intent)
                requireActivity().finish()
            }
        }

Outro

  • 이제 곧 최종프로젝트를 앞두고있다. 여기에 kt입사지원도 내일까지라 오늘밤에 어떻게든 자소서를 끝내려고하는데 이게 참 허허.. 쉽지않다..! 여기에 MVVM 공부도 있으니..!!!
  • 그래도 프로젝트에 구조를 하나씩 쌓아나가는것은 여전히 재미있다. 그리고 이것은 더많은 요구사항을 반영하는데 좋은 구조를 잡는 공부이기도하니 내가 입사할 회사도 나도 모두가 해피한 일이다 :) 오늘도 달리고 내일도 달리고 힘내자!!
profile
잘부탁드립니다!

0개의 댓글