Notice
Recent Posts
Recent Comments
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

코린이 탈출기

[RxJava] Giphy API를 활용한 gif 추천 애플리케이션 (3) 본문

Android 공부

[RxJava] Giphy API를 활용한 gif 추천 애플리케이션 (3)

명란파스타 2022. 3. 17. 16:30

한번에 20개의 gif를 가져오고 있는 현재 애플리케이션을 무한스크롤로 변경해보는 작업을 해보자 !!

 

이전 게시글

 

[RxJava] Giphy API를 활용한 gif 추천 애플리케이션 (2)

이전 게시글에서는 RxJava를 사용하여 Giphy API로 랜덤으로 받아온 gif를 recyclerView에 띄워주는 방법에 대해 다루어 보았다. Giphy API 사용법: https://developers.giphy.com/docs/api/endpoint/#trending 이..

ekdbsl22.tistory.com

 

무한 스크롤이란?

1. recyclerView로 보여줄 목록을 구현하고 (이전 게시글에서 작업한 부분)

2. 스크롤이 끝에 닿았을 경우 새로운 데이터를 불러온다.

3. 새로운 데이터를 adapter 통해서 띄워준다.

 

보여줄 항목이 많은 경우 이 항목들을 모두 한번에 불러와서 저장하고 있다가 스크롤할때 보여주는 것은 비효율적이고, 처음 로딩 시 시간이 매우 오래걸리기 때문에 현재 보여줘야 할 데이터들을 그때그때 불러와서 보여주는 방식으로 구현하도록 하자.

 

2. 스크롤이 끝에 닿았을 경우 새로운 데이터 불러오기

binding.giphyListView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        val layoutManager = recyclerView.layoutManager as GridLayoutManager
        val lastVisibleLine = (layoutManager.findLastVisibleItemPosition() + 1) / COLUMN_NUM
        val itemTotalLine = adapter.itemCount / COLUMN_NUM

        val searchText = binding.searchEditText.text.toString()

        if (searchText.isNotBlank() && lastVisibleLine == (itemTotalLine - SCROLL_LINE_OFFSET)) {
            viewModel.getSearchGiphyList(searchText, LOAD_COUNT)
        }
    }
})

우선, GiphyListFragment의 initView() 함수 내에 목록을 보여줄 recyclerView에 scrollListener를 추가해준다.

나는 항목들을 grid로 보여주고 있기 때문에 가장 마지막에 보이는 line과 전체 항목 line을 계산해주었다.

 

searchText가 비어있지 않고, 가장 마지막에 보이는 line이 전체 항목 line에서 offset을 뺀 것과 같으면 새로운 리스트를 다시 불러온다.

이렇게 해주는 이유는 가장 마지막까지 스크롤하기 전에 미리 리스트를 불러와서 스크롤이 끊기지 않고 보여지게 하기 위함이다.

 

그런데 새로운 리스트를 불러올 때에는 기존에 불러왔던 리스트 이후의 것들을 불러와야 할텐데, 이 작업은 어떻게 해주어야 할까?

// GiphyListViewModel
fun getSearchGiphyList(searchQuery: String, count: Int) {
    disposeBag.addExclusive(giphyApiClient.getSearchGiphyList(searchQuery, count, giphyList.size)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            { response ->
                giphyList.addAll(response.data)
                giphyListLiveData.value = giphyList
            },
            {
                Log.e(TAG, "getSearchGiphy error: response is not successful or response.body is null")
            }
        ))
}
// GiphyApiClient
fun getSearchGiphyList(searchQuery: String, limit: Int, offset: Int): Single<GiphySearchResponse> {
    return giphyApi.getSearchGifList(Constants.apiKey, searchQuery, limit, offset)
}

이렇게 Offset 값을 이용해주면 된다.

GiphyApi에서는 offset과 limit을 통해 읽어올 리스트의 시작점, 그리고 개수를 정할 수 있도록 하고 있다.

 

현재 giphyList의 size를 Offset으로 지정하면, getSearchGiphyList를 호출할 때마다 기존 리스트에 이어서 데이터들을 가져올 수 있게 된다.

 

3. 새로운 데이터를 Adapter 통해 띄워주기

새로운 giphyList 가져오기를 완료한 경우, 이 데이터들을 adapter를 통해 목록에 보여주어야 한다.

이 때 DiffUtil 이라는 안드로이드 유틸리티 클래스를 사용해보았다.

 

DiffUtil이란?

DiffUtil이란 List Item이 변경되었을 때 이전 List Item과 새롭게 변경된 List Item을 비교해서 업데이트 작업을 수행하는 유틸리티 클래스이다. 변경된 Item만 업데이트 하는 방식으로 이전에는 List Item이 변경되면 다 다시 그리기 시작했다면, 이제는 변경된 Item만 다시 그리는 방식을 하고 있어서, 효율적으로 퍼포먼스를 낼 수 있다.

 

ListAdapter

DiffUtil을 활용해서 리스트를 업데이트 할 수 있는 기능을 추가한 Adapter

 

사용가능한 메서드

  • getCurrentList(): 현재 리스트를 반환한다
  • onCurrentListChanged(): 리스트가 업데이트 됐을 때 실행할 콜백을 지정할 수 있다
  • submitList(List): 리스트 데이터를 갱신할 때 사용
class GiphyItemAdapter : ListAdapter<Gif, GiphyItemAdapter.ViewHolder>(diffUtil) {
    lateinit var blinkIndexList: List<Int>
    inner class ViewHolder(
        private val binding: GiphyItemLayoutBinding
    ): RecyclerView.ViewHolder(binding.root) {

        fun bind(item: Gif?) {
            Log.d("giphyTest", "url: ${item?.images?.fixed_height?.url}")
            Glide.with(binding.root)
                .asGif()
                .placeholder(R.drawable.loading_icon)
                .load(item?.images?.fixed_height?.url)
                .into(binding.giphyImage)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = GiphyItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
        if (blinkIndexList.isNotEmpty()) {
            if (blinkIndexList.contains(position)) {
                holder.itemView.visibility = GONE
            } else {
                holder.itemView.visibility = VISIBLE
            }
        } else {
            holder.itemView.visibility = VISIBLE
        }
    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<Gif>() {
            override fun areContentsTheSame(oldItem: Gif, newItem: Gif) =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: Gif, newItem: Gif) =
                oldItem.id == newItem.id
        }
    }
}
// GiphyListFragment
private fun initViewModel() {
    viewModel.giphyListLiveData.observe(viewLifecycleOwner) {
    	// viewModel의 데이터 변경이 감지되면 새로운 데이터로 갱신해주기 위해 submitList 호출
        giphyItemAdapter.submitList(it.toMutableList())
    }
}

DiffUtil.ItemCallback에서 구현 필요한 두 가지 함수가 있다.

areContentsTheSame: 이전 아이템과 새로운 아이템을 비교하여 데이터의 갱신은 없었는지 확인하는 함수

areItemsTheSame: 두 아이템이 동일한 아이템인지 확인하는 함수. 보통 고유한 id를 기준으로 비교함