본문 바로가기

안드로이드

무한스크롤을 구현해보자!!!

무한스크롤이란게 뭘까?

무한스크롤은 한마디로 pageination(서버쪽에서 이렇게 많이 부르는거 같다),paging 이라고 불리는 기법을 이용하는것이다.

예시를 들자면 인스타에서 피드를 계속 스크롤 해보면 한도끝도 없이 나온다. 근데 이 피드를 최초 액티비티를 켰을때 모두다 가져오는 것일까? 아닐것이다. 데이터는 진짜 끝도없이 서버에 쌓여있을테고 그걸 화면에 표시할 만큼만 들고와서 화면에 표시해주고 특정조건 즉 화면에 표시한 정보들을 사용자가 다 읽었을때 새로운 정보를 가져와서 다시 뿌려줄것이다.

 

물론 처음에 데이터를 왕창 가져와서 한번만 통신하고 끝낼수도있을것이다. 하지만 많이 가져오는만큼 서버와 통신하는 시간이 길어질테고 그것을 담는 메모리또한 크기때문에 메모리 리소스도 효율적으로 사용하는 방식은 절대 아닐것이다.

 

그래서 페이징을 통해 얻는 장점은 뭘까?

1.  데이터를 통신할때 오가는 데이터의 크기가 줄어들기 때문에 통신시간이 빨라진다.

2. 사용자가 실제로 사용하는 만큼의 데이터만 들고있을수 있게된다.(메모리 리소스 효율적 사용)

3. 이부분은 의견이 분분하지만 사용성이 좋다고 느끼는 사람이 많다고한다(근데 무한스크롤 불편하다고 )

-> 불만의 이유 : 과거의 글을 찾으려면 계속 내려서 있는곳까지 직접 찾아야하기 때문에 원하는 인덱스 페이지의 글을 찾는데 너무 번거로운 과정을 거쳐야한다는 단점이있다.

 

 

 

이러한 방식을 구현하는 방법은 크게 2가지이다. 안드로이드 jetpack에서 제공하는 paging3 라이브러리를 사용하던가 

그냥 코드에서 가져온 정보를 다읽는 시점에 정보를 가져오는것을 직접 구현하는 방법이다.

 

물론 paging3 라이브러리를 이용하는것이 지원하는것도 많고 참 좋을것이다. 하지만 paging3 는 coroutine flow개념을 알아야하기에 flow 개념을 모르는 나에게는 현재 무리이다.그래서 추후에 공부한다면 한번 다시 다뤄볼 예정이다.

 

이 글에서는 코드로 직접 구현하는 것을 살펴볼것이다.

 


 

서버쪽에서 받아오는것도 방식이 3가지나 되기 때문에 간단히 알아보고 서버 개발자랑 소통해보자!!!!

뭐 물론 이건 사실 클라 입장에서 깊게 볼일도 아니고 무한스크롤과는 사실상 상관없지만 서버통신을 해서 받아오니까 서버쪽에서는 페이지네이션을 어떤 방식으로 하는지 어떤 장단이있는지도 겉핥기로 알고 넘어가자!!(똘똘한 개발자가 되기 위하여)

 

일단 서버쪽 용어는 살짝씩 다르고 하기때문에 무한스크롤,페이징 이렇게 하루종일 검색해봐야 잘 안나온다.

-> 검색어는 커서 기반 페이지 네이션, 오프셋 기반 페이지네이션 이런 검색어로 구글링해야한다.

 

일단 클라입장에서 어떻게 받아오는지는 3가지로 나뉜다

 

offset 기반

1.page 방식

/article/?page=1

2.offset/limit 방식

/article/?offset=10&limit=10

 

사실 이 두방식은 같다고 봐도 무방하다 

offset/limit 방식은 limit를 클라에서 몇개 받아올껀지 정해주는데

사실 page 방식은 서버에 그 limit가 박혀있는거라고 봐도 무방하다(그니까 그냥 같은거임)

 

page에서 저 숫자부분은 1번페이지를 받아오는건지 offset처럼 데이터가 시작하는 위치(index)를 넣어줄껀지는 개발을 어떻게 하냐에 따라 다르다고한다. 즉 서버개발자한테 어떻게 구현할껀지 물어보자

 

그래서 offset기반 방식이 어떻게 하는건데 씹덕아 라고 묻는다면. 한줄요약으로 이렇게 설명할수있다

  • DB의 offset 쿼리를 사용하여 '페이지' 단위로 구분하여 요청/응답하게 구현

이게 대충 휘뚜루 마뚜루 설명하자면 오프셋 기반은 우리가 원하는 데이터가 몇번쨰에 있다는것에 집중한다(index) 즉 몇번 인덱스로부터 다음으로 몇개 주세요이다.

 

이래서 이방식에는 두가지 문제점이 있는데


문제1. 각각의 페이지를 요청하는 사이에 데이터의 변화가 있는 경우 중복 데이터 노출

예를 들어, 1페이지에서 20개의 row를 불러와서 유저에게 1페이지를 띄워줬습니다. 고객이 1페이지의 상품들을 열심히 보고 있는 사이, 항상 열심히 일하고 있는 상품 운영팀에서 5개의 상품을 새로 올렸네요? 유저는 1페이지 상품들을 다 둘러보고 2페이지를 눌렀어요. 그럼 어떻게 될까요?
유저는 이미 1페이지에 보았던 상품 20개중 마지막 5개를 다시 2페이지에서 만나게 됩니다.
가끔 유저들의 활동이 활발한 커뮤니티에서 게시글을 쭉 읽다보면 이런걸 경험한 적이 있으실거에요. 그럼 '아, 이 사이트는 커서 기반 페이지네이션'이 구현되지 않았구나' 라고 생각하시면 됩니다. (ㅋㅋ)

문제2. 대부분의 RDBMS 에서 OFFSET 쿼리의 퍼포먼스 이슈

우리의 DB도 모든 정렬 기준(ORDER BY)에 대해 해당 row가 몇 번째 순서를 갖는지 알지 못합니다. 따라서 offset 값을 지정하여 쿼리를 한다고 했을 때 임시로 해당 쿼리의 모든 값들을 전부 만들어놓은 후 지정된 갯수만 순회하여 자르는 방식을 사용하게 되지요. offset이 작은 수라면 크게 문제가 되지 않지만 row 수가 아주 많은 경우 offset 값이 올라갈 수록 쿼리의 퍼포먼스는 이에 비례하여 떨어지게 되어 있습니다.

출처 : minsangk velog

 

뭐한마디로 정리하자면 첫화면이 로드되고 목록에 내용이 추가되면 추가된만큼 중복된 글을 접하게 된다는것이다.

 

 

Cursor기반

그래서 커서기반 페이지네이션이 등장했다 -> 중복되는 데이터가 오는 것을 처리할수있는 방법이다.

 

spark 에 이 방식이 적용되어있는데

 

받아오는 방식은 이러하다

/room?lastId=&size=S

이런식으로 lastId와 size 를 넘겨서 받는다 이때 항상 마지막으로 오는 아이템의 id값을 저장해놨다가 보내주는 방식인데 이를 위해서 코드를 보면 lastId를 저장하는 부분이 있다.

 

오프셋 기반 페이지네이션은 우리가 원하는 데이터가 '몇 번째'에 있다는 데에 집중하고 있다면, 커서 기반 페이지네이션은 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는 데에 집중합니다. n개의 row를 skip 한 다음 10개 주세요가 아니라, 이 row 다음꺼부터 10개 주세요를 요청하는 식이지요.

출처 : minsangk velog

 

그래서 결국  요약을해보자면 id값을 주면 서버에서는 이 id아이템을 기준으로 몇개를 보내주세요가 된다 그래서 중복되는 아이템이 올일이 없다고한다.

 

이런거 내부적으로 어떻게 구현하는지 이런거는 서버쪽에서 해줄일이고 

우리는 이방식을 이용한다면 중복되는 아이템을 받아올일이 없다는 장점이있으니 이걸쓰면된다는것을 인식하면되고

 

lastId 를 요청마다 넘겨줘야하니 받아온 데이터에 마지막 아이템에서 Id를 추출해서 저장해놨다가 요청할때 사용하는 부분을 구현하면 된다는것을 인식하면 될것이다.

 

잡설이 길었고 이제 구현한것을 봐보자

 


구현 방법 및 코드 분석

일단 기존에 무한스크롤을 적용하지 않고 그냥 리스트를 100개씩 불러왔던 기존코드를 봐보자

 

HomeMainViewModel.kt

fun getHomeAllRoom(lastid: Int, size: Int) {
    viewModelScope.launch {
        _isLoading.value = true
        homeRepository.getHomeAllRoom(lastid, size)
            .onSuccess {
                _roomList.postValue(it.data.rooms)
            }.onFailure {
                Log.d("Home_main_error_get_list", it.message.toString())
            }
    }
}

그냥 받아와서 넣어준다 이내용 밖에 없다

 

HomeMainFragment.kt

private fun updateHomeRecyclerViewAdapter() {
    homeMainViewModel.getHomeAllRoom(-1, 100)
    homeMainViewModel.roomList.observe(viewLifecycleOwner) {
        homeRecyclerViewAdapter.updateHomeList(it)
        homeMainViewModel.updateIsLoading()
    }
}

여기서도 그냥 무식하게 100개 받아오고 받아오는 변수에 옵저버 달아서 데이터가 오면 어댑터 리스트 최신화 시켜주고 로딩 로티 끄는 코드만 있다.

 

 

이제 무한스크롤 구현하는것을 보기 이전에

 

구현방법을 대략적으로 이런순서를 거쳐야 한다는것을 나열해 보겠다.

0.(스파크에서만 적용된것) 멀티뷰타입을 가져가야하는데 기존에 이미 멀티뷰타입 을 구분하는것이 있어서 변수를 하나 만들어줘서 구분할수있게 response에 변수 하나 달아줬다.

1.로딩할때 밑에다 붙여줄 아이템 즉 로티나 스피너 같은 것만 들어있는 로딩 아이템을 하나 만들어준다. 

2.어댑터에서 뷰홀더 하나더 만들어주고 getItemVIewType,onCreateViewHolder 에서 로딩 관련 들어가는 것을 분기처리해준다

3.viewModel에서 로딩관련 더미 데이터 하나 만들어주고 기타 변수 선언하고 관련 분기처리한다.

4.fragment에서 불러줄때 onScrollListener에 붙여주고 리스폰스가 들어오는 라이브데이터에 옵저버 붙여서 listadapter 리스트 최신화 시키는 코드를 만들어준다.

 

대략적인 과정은 

일단 스크롤이 최하단으로가면 아무것도 없는 리스트 아이템을 하나 넣어서 그거들어왔을때는 로딩 뷰를 최하단에 띄워줬다가 로딩이 완료되면 최하단 로딩하는 아이템을 뜯어내고 새로들어온 리스트를 추기해주는 방식이다.

 

이과정을 거친다면 무한스크롤 완료!!

 

 

신나버려!!!!!!

 

어쩃든 0번부터 이제 코드와 함께 살펴보자

 

0.구분변수 달기

HomeResponse.kt

package com.spark.android.data.remote.entity.response

data class HomeResponse(
    val rooms: List<Room>
)

data class Room(
    val doneMemberNum: Int,
    val myStatus: String?,
    val isStarted: Boolean,
    val leftDay: Int?,
    val life: Int,
    val memberNum: Int,
    val profileImg: List<String>?,
    val roomId: Int,
    val roomName: String,
    val isUploaded: Boolean,

	//이거 새로 추가한거
    val infiniteLoading : String = ""
)

원래 홈에서 멀티뷰타입을 사용중인데다가 딱히 분기처리할만한 적절한 값이 없었어서

infiniteLoading 이라는 변수를 기본값을 "" 로 놓고 필요한 시점에만 loading 으로 줘서 구분하려는 의도로 변수를 만들었다.

-> 레트로 핏에서 들어오는거 이외에 내가 쓰려는 용도로 변수 추가해도  된다 response에!!

 

1.로딩시 들어갈  아이템  만들기

로딩이 있을때 즉 맨아래 까지 스크롤 했을때 잠시 로딩시간동안 리사이클러뷰 최하단에 붙여줄 모양의 로딩 아이템을 만들어야한다.

그래야 로딩동안 어색하지 않기 때문에 

 

이건 딱히 별거는 없고 그냥 로딩이라는것만 표시해주는것을 만들어주자

 

item_home_loading.xml

대충 중앙에 로딩로티 하나 들어있다.

 

2.어댑터 뜯어 고치기

HomeRecyclerViewAdapter.kt

package com.spark.android.ui.home.adapter

import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.spark.android.data.remote.entity.response.Room
import com.spark.android.databinding.ItemHomeLoadingBinding
import com.spark.android.databinding.ItemHomeRecyclerviewBinding
import com.spark.android.databinding.ItemHomeRecyclerviewWaitingBinding
import com.spark.android.ui.habit.HabitActivity
import com.spark.android.ui.home.adapter.multiview.INFINITE_LOADING
import com.spark.android.ui.home.adapter.multiview.TICKET_STARTED
import com.spark.android.ui.home.adapter.multiview.TICKET_WAITING
import com.spark.android.ui.waitingroom.WaitingRoomActivity
import java.lang.RuntimeException

class HomeRecyclerViewAdapter(
    private val finishRoomEvent: ((Int, String) -> Unit)? = null
) : ListAdapter<Room, RecyclerView.ViewHolder>(homeDiffUtil) {

    private lateinit var itemHomeRecyclerviewBinding: ItemHomeRecyclerviewBinding
    private lateinit var itemHomeRecyclerviewWaitingBinding: ItemHomeRecyclerviewWaitingBinding
    private lateinit var itemHomeLoadingBinding: ItemHomeLoadingBinding

    override fun getItemViewType(position: Int): Int {
        return if (getItem(position).infiniteLoading == "loading") {
            INFINITE_LOADING
        } else {
            if (getItem(position).isStarted) {
                TICKET_STARTED
            } else {
                TICKET_WAITING
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            TICKET_STARTED -> {
                itemHomeRecyclerviewBinding = ItemHomeRecyclerviewBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                HomeRecyclerViewHolder(itemHomeRecyclerviewBinding)
            }
            TICKET_WAITING -> {
                itemHomeRecyclerviewWaitingBinding = ItemHomeRecyclerviewWaitingBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                HomeRecyclerViewWaitingHolder(itemHomeRecyclerviewWaitingBinding)
            }
            INFINITE_LOADING -> {
                itemHomeLoadingBinding = ItemHomeLoadingBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                HomeRecyclerViewLoadingViewHolder(itemHomeLoadingBinding)
            }
            else -> {
                throw RuntimeException("알 수 없는 viewType error")
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is HomeRecyclerViewHolder) {
            holder.onBind(getItem(position), finishRoomEvent)
        } else if (holder is HomeRecyclerViewWaitingHolder) {
            holder.onBind(getItem(position))
        }
    }


 
    class HomeRecyclerViewLoadingViewHolder(val binding: ItemHomeLoadingBinding) :
        RecyclerView.ViewHolder(binding.root)

    
}

 

일단 더 길었는데 간단하게 하기위해 뷰홀더 기존에있던거 두개는 여기서는 생략했고

디프유틸 관련코드도 다 들어냈다 만약 궁금하다면 스파크 코드보자 ㅎ

 

그래서 순서대로 살펴보자면

 

우선 getItmeViewType에서 들어오는 아이템으로 어떤 뷰홀더로 넘겨줄지 분기처리해준다

-> 기존에 isStarted 변수로 분기 처리해줬고 여기서 적절하게 분기처리할 만한 변수가 없어서 아까 infiniteLoading를 만들었으니 그걸 통해서 분기처리해서 infiniteLoading 값이 loading이면 로딩 아이템이 사용되고 기본값인 ""가 들어오면 넘어가는 것이다.

 

다음 onCreateViewHolder 에서 viewType 넘어오는거로 어떤 아이템을 그려서 반환할지 작성한다.

 

onBindViewHolder에는 딱히 아무것도 추가해줄게 없는게 데이터가 들어가는 부분 자체가 없어서 뷰홀더에 onbind 함수도 안만들었기에 넘어가도된다

 

마지막으로 뷰홀더를 loading item 에 맞게 만들어준다.

 

이러면 이제 리사이클러뷰 어댑터 분기처리도 끝이다.

 

 

3.뷰모델 데이터 가져오는거 수정 (분기처리)

이제부터가 핵심이라고 볼수있다

 

HomeMainViewModel.kt

package com.spark.android.ui.home.viewmodel

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.spark.android.data.remote.entity.response.Room
import com.spark.android.data.remote.repository.HomeRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class HomeMainViewModel @Inject constructor(
    private val homeRepository: HomeRepository
) : ViewModel() {

    private val _roomList = MutableLiveData(listOf<Room>())
    val roomList: LiveData<List<Room>> = _roomList

    private val _isLoading = MutableLiveData(false)
    val isLoading: LiveData<Boolean> = _isLoading

    var lastId = -1
        private set

    var hasNextPage = true
        private set

    var isAddLoading = false
        private set

    private val loadingItem = Room(
        doneMemberNum = -1,
        myStatus = null,
        isStarted = false,
        leftDay = -1,
        life = -1,
        memberNum = -1,
        profileImg = null,
        roomId = -1,
        roomName = "로딩용 더미 방 데이터",
        isUploaded = true,
        infiniteLoading = "loading"
    )

    fun updateIsLoading(isLoading: Boolean) {
        _isLoading.postValue(isLoading)
    }


    fun getHomeAllRoom() {
    	//1
        if (requireNotNull(roomList.value).isEmpty()) {
            updateIsLoading(true)
        }
        //2
        if (requireNotNull(roomList.value).isNotEmpty() && hasNextPage) {
            isAddLoading = true
            addLoadingItem()
        }
        viewModelScope.launch {
        	//3
            homeRepository.getHomeAllRoom(lastId, LIST_LIMIT)
                .onSuccess { response ->
                	//4
                    val tempHomeList = response.data.rooms
                    
                    //5
                    if (tempHomeList.isNotEmpty()) {
                        lastId = tempHomeList.last().roomId
                    }
                    //6
                    if (tempHomeList.size < LIST_LIMIT && lastId != -1) {
                        hasNextPage = false
                    }
                    //7
                    isAddLoading=false
                    updateIsLoading(false)
                    //8
                    _roomList.postValue(
                        requireNotNull(_roomList.value).toMutableList().apply {
                            remove(loadingItem)
                            addAll(tempHomeList)
                        }
                    )
                }.onFailure {
                    Log.d("Home_GetHomeAllRoom",it.message.toString())
                }
        }
    }

    private fun addLoadingItem() {
        _roomList.value = requireNotNull(_roomList.value).toMutableList().apply { add(loadingItem) }
    }

    companion object {
        private const val LIST_LIMIT = 6
    }
}

 

여기서도 잡다한 부분은 생략했다

 

이부분은 getHomeAllRoom 함수를 중점적으로 보면된다.

 

1.우선 시작하자마자 roomList 를 검사해서 빈 리스트라면 중앙에 있는(무한스크롤 로딩말고 그냥 전체 로딩) 로딩을 보여주는 로직이있다.

 

2.이제roomList랑 가 비었는지 여부를 확인하고 다음페이지가(hasNextPage) 있는지 확인해서 isAddLoading(프래그먼트에서 로딩조건 판단하는데 쓰임) 을 true로 바꿔주고  리스트에 로딩아이템 을 추가해주는 함수인 addLoadingItem을 호출해준다 -> 로딩아이템이 리스트로 들어간다.

->로딩아이템은 위에서 더미로 infiniteLoading 값만 loading으로 해주고 나머지는 null아니면 없을 법한 형태로 만들어준다 -1같은거 넣어서(그래야 문제생겨도 확인이 쉽다)  

 

3.우선 서버통신을 해서 받아오는데 여기서lastId는 초기값은 -1로 시작하는거로 서버랑 이야기했기 때문에 lastId는 -1로 처음에 들어가고 나중에 lastId를 리스트를 받아올때마다 최신화시키는 로직이있다.

그리고 LIST_LIMIT는 size 즉 받아올 데이터의 갯수이다.

 

4.일단 받아온 리스폰스 temp로 담아준다 (처리해야할것들이 살짝씩있어서)

 

5.여기서 받아온 리스트가 있다면 그거의 마지막 아이템의 id를 lastId에 넣어주는 로직이있다.

 

6.여기서는 lastid가 -1이 아니고(이미 리스트를 한번이상 불러왔고) 받아온 리스트의 사이즈가 내가 불러올때 사용한 LIST_LIMIT 보다 작으면 즉 리스트 끝까지 갔으면 서버에있는 리스트를 다 불러온거니까 불러올 데이터가 더있다는 변수인 hasNextPage 를 false로 만들어준다 

 

7.여기선 로딩관련된것들을 다꺼주는 것이다.

fragment에서 데이터를 불러오는 판단요소인 isLoading 을 false로 만들어주고 

중앙에 있는 로딩 로티 꺼주는 updateIsLoading 을 호출해준다.

 

8.받아온 데이터를 뷰모델에 변수인 roomList에 담아주는데 아까추가한 loadingItem 은 제거하고 받아온것들을 담아준다.

 

 

4.액티비티(프래그먼트)에서 스크롤리스너 달아주기

HomeMainFragment.kt

@AndroidEntryPoint
class HomeMainFragment : BaseFragment<FragmentHomeMainBinding>(R.layout.fragment_home_main) {

    private lateinit var homeRecyclerViewAdapter: HomeRecyclerViewAdapter
    private val homeMainViewModel by viewModels<HomeMainViewModel>()
    private lateinit var toastAnimation: Animator

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.homeViewModel = homeMainViewModel
        homeMainViewModel.getHomeAllRoom()

        initHomeRecyclerViewAdapter()
        addScrollListenerToHomeRv()
        initHomeListObserver()
        initMyPageBtnClickListener()

    }

    

    private fun addScrollListenerToHomeRv() {
        binding.rvHomeTicket.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                val layoutManager = binding.rvHomeTicket.layoutManager as LinearLayoutManager
                val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
                if (homeMainViewModel.hasNextPage) {
                    if (!homeMainViewModel.isAddLoading && layoutManager.itemCount <= lastPosition + LOAD_POSITION &&
                        !binding.rvHomeTicket.canScrollVertically(STATE_LOWEST)
                    ) {

                        homeMainViewModel.getHomeAllRoom()
                    }
                }
            }
        })
    }

    private fun initHomeListObserver() {
        homeMainViewModel.roomList.observe(viewLifecycleOwner){
            homeRecyclerViewAdapter.updateHomeList(it)
            if(homeMainViewModel.isLoading.value == true){
                homeMainViewModel.updateIsLoading(false)
            }
        }
    }

    

    companion object {
        private const val LOAD_POSITION = 3
        private const val STATE_LOWEST = 1
    }
}

 

자 이제 마지막 단계이다 좀만 더 집중하면된다

 

이제 리사이클러뷰에다 OnScrollListener 를 붙여서 스크롤 할때마다 불러와야하는지 조건을 판단하고 조건에 맞다면 데이터를 불러오는것이다.

 

자이제 살펴봐야 할것이 lastposition은 화면에 나타나는 가장 마지막 아이템의 인덱스 값이다.

 

분기처리로

일단 hasNextPage로 불러올것이 있는지 판단하고

다음 isAddloading 으로 로딩아이템이 붙어있는지 상태확인하는부분이고

layoutManager.itemCount <= lastPosition + LOAD_POSITION 으로 itemCount 는 리스트의 전체 아이템 갯수 

lastPosition은 현재 표시된 가장마지막 아이템의 포지션 그리고 LOAD_POSITION은 내가 지정한 끝에 도달하기전 몇번째 아이템이 보일때 정보를 불러올지 정해주는 상수 이다  즉 lastPosition 은 점점 커지고 마지막에 도달하기 3개전 아이템에서 불러오는 시점으로 설정되어있다.

!binding.rvHomeTicket.canScrollVertically(STATE_LOWEST) 즉 canScrollVertically는 스크롤 가능한상태인지(끝에 도달했는지) 여부를 boolean 값으로 주기때문에 최하단에 도달하기전까지 true를 배출하다 최하단에 도달하면 false를 반환한다 

그리고 안에 들어가는 매개변수는 Vertically 기준으로 -1이 위쪽, 1이 아래쪽이다.(최상단 최하단 구별용)

 

이렇게 3가지 조건을 통해서 받아와야하는지 판단해서 데이터를 받아온다.

 

데이터를 받아온후 데이터를 받아온거에대해 옵저버를 붙여서 어댑터에 리스트를 최신화 시켜주는 코드도 달고

이때 전체로딩 여부를 확인해서 로딩이 켜져있다면 로딩을 끄는 코드도 넣는다.

 

자 이렇게 엄청나게 기나긴 무한스크롤 코드를 살펴보았다 !!! 

 

끝!!!!!!!!!!!!!!!!!!!!