본문 바로가기
개발노트/Kotlin

12주차 3일 무한스크롤, 자바 라이브러리의 null예외처리

by 시계속세상은아직돌아가는중 2023. 9. 27.

1. 무한 스크롤

 

무한 스크롤을 구현하기 위해서 addOnScrollListener로 스크롤을 감지해야한다.

 

       homeRvVideoList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                val layoutManager = recyclerView.layoutManager as LinearLayoutManager

                val visibleItemCount = layoutManager.childCount
                val totalItemCount = layoutManager.itemCount
                val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()

                val isAtEndOfList = visibleItemCount + firstVisibleItemPosition >= totalItemCount

//                !homeRvVideoList.canScrollHorizontally(1)

                if (isAtEndOfList) {

                    getNextPage() // 주석 시 홈 api 사용 x

                }
            }
        })

 

여기서 주석처리한 .canScrollHoriznotally(1)은 스크롤이 맨 마지막에 도착했는지에 대한 판정을 해주는 식이었으나

해당 값을 사용하면 에러 발생하였다.

 

추정하기론, 스크롤 이벤트는 굉장히 민감하고 맨 마지막 스크롤이 여러번 되면서 굉장히 많은 호출이 되었던 것 같다.

 

이는 isAtendOfList라는 bool식을 만들어 준 이후에도 그러했다.

 

 

다음 페이지를 호출하는 함수

fragment

  private fun getNextPage() = with(viewModel) {
        viewModel.setNextPage()
    }

viewmodel

    private val _categoryList: MutableLiveData<Set<PlayListModel>> = MutableLiveData()
    val categoryList: LiveData<Set<PlayListModel>> get() = _categoryList
    
                   ~~~~~~
                      기존코드
                           ~~~~~~
                           

   fun setNextPage() {
        viewModelScope.launch {
        
            val token = pageToken.value
            val response = repository.getPopularVideo(token)
            val list = response.first
            val nextToken = response.second
            val currentList = categoryList.value.orEmpty().toMutableSet().apply {
                addAll(list)
            }
            _pageToken.value = nextToken
            _categoryList.value = currentList

        }
    }

1) 여려번 호출되는 문제를 해결하기 위해 자료구조형 'set'을 사용함

 

set은 중복을 알아서 처리해주는 집합형 자료구조형이다.

따라서 여러번 같은 값이 들어오는 것이 문제라면 set형으로 해당 방식을 막을 수 있다.

 

여기서 발생하는 문제

 

-> 근본적인 문제인 스크롤 이벤트가 예민하게 감지될 때 마다 api가 호출되는 것은 막지 못함.

 

2) 여러번 호출 할 수 없도록 state를 만들어서 관리하자!

 

viewmodel

    private val _loading: MutableLiveData<Boolean> = MutableLiveData()
    val loading: LiveData<Boolean> get() = _loading
    
        fun setNextPage() {
        viewModelScope.launch {
            if (_loading.value == true) return@launch
            _loading.value = true

            val token = pageToken.value
            val response = repository.getPopularVideo(token)
            val list = response.first
            val nextToken = response.second
            val currentList = categoryList.value.orEmpty().toMutableSet().apply {
                addAll(list)
            }
            _pageToken.value = nextToken
            _categoryList.value = currentList

            _loading.value = false
        }
    }

 

loading이라는 bool식을 만들어서 첫 실행 시 loading.value를 true(로딩중이다)로 만든다.

이후 loading이 끝난 맨 마지막에 해당 값을 false로 세팅해준다.

 

그렇다면! 처음에 loading의 값이 true라면 return하여 다시 재실행할 수 없도록 만들어준다!

 

이로써 로딩이 구현되었으며 해당 값이 호출됨으로써 로딩의 true-false간격에 로딩 화면을 구상할 수 있게 되었다.

 

2. 자바 라이브러리의 null 체크

fragment에서

   private fun initViewModel() {
        with(viewModel) {
            categoryList.observe(viewLifecycleOwner) {
                if (it != null) {
                    videoAdapter.submitList(it.toList())
                    Log.d("리스폰", categoryList.value.toString())
                }
            }
            pageToken.observe(viewLifecycleOwner) {
                if (it != null) {
                    Log.d("토큰", pageToken.value.toString())
                    //호출 횟수 테스트용 observe
                }
            }
            loading.observe(viewLifecycleOwner) {
                if (it != null) { // 뷰 모델이 자바로 구성되어 있기 때문. 그런 경우 방어 코딩을 해줘야함.

                    if (it == true) {
                        //로딩 화면을 구현할 예정
                    } else {
                        //로딩 화면을 지우는 로직 구성 해야함
                    }

                }
            }
        }
    }

뷰 모델 사용 시 observe해당 람다식으로 표현될 수 있는 it들에 대한 null처리를 해줘야 한다.

 

그렇다면 왜 해줘야할 까?

 

 

뷰 모델의 안으로 들어가면 해당 코드가 java로 작성되었음을 알 수 있다.

java는 kotlin과 다르게 언제든지 null이 들어올 수 있으므로, java로 작성된 라이브러리를 사용할 때는 해당 값에 null이 들어오는지 체크할 수 있는 방어 코딩이 필요한 것이다!

 

이는 현재 쓰고있는 retrofit,glide에도 똑같이 적용 해야할 것 이다.

 

뷰 모델을 제외하고는 지금까지 적용하고는 있었지만, 이게 자바 코드라서가 아니라 구조 자체가 null이 들어오면 오류가 발생하는 구조였기 때문이다.

 

비슷한 이유지만 느낌적 느낌이 살짝 다르다.

 

정리하자면

 

java로 작성된 라이브러는 null값이 변수 선언 시 그냥 들어올 수 있다

-> kotlin은 ?체크를 해줘야 들어올 수 있다

-> 그렇다면 null체크를 함으로써 kotlin 문법상 충돌을 방지한며, null을 컨트롤 하기 편하게 만든다.

=> null safety하다

 

null이 들어와서는 안되는 기능에 null이 들어와서 앱이 죽었다.

->그렇다면 방어 코드를 작성해서 방어를 해준다

 

이런 흐름인 것이다.