250x250
    
    
  
														Notice
														
												
											
												
												
													Recent Posts
													
											
												
												
													Recent Comments
													
											
												
												
											
									| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 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 | 
													Tags
													
											
												
												- 막내의막무가내 회고 및 목표
- 막내의막무가내 코틀린
- 막내의 막무가내
- 막내의막무가내 프로그래밍
- 안드로이드 sunflower
- 막내의막무가내 rxjava
- flutter network call
- 막내의막무가내 일상
- 막내의막무가내 목표 및 회고
- 막내의막무가내 안드로이드 코틀린
- 막내의막무가내 SQL
- 막내의막무가내 코틀린 안드로이드
- 막무가내
- 부스트코스
- 2022년 6월 일상
- 부스트코스에이스
- 막내의막무가내 플러터 flutter
- 프로그래머스 알고리즘
- 막내의막무가내 코볼 COBOL
- 안드로이드
- 프래그먼트
- 주엽역 생활맥주
- 막내의막무가내 알고리즘
- 막내의막무가내
- Fragment
- 막내의막무가내 플러터
- 안드로이드 Sunflower 스터디
- 막내의막무가내 안드로이드
- 막내의막무가내 안드로이드 에러 해결
- 막내의 막무가내 알고리즘
													Archives
													
											
												
												- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[안드로이드] 코틀린 리사이클러뷰 무한 스크롤 구현 본문
728x90
    
    
  
[2021-04-13 업데이트]

영화 불러오는데 무한 스크롤이 필요하여 해당 기능을 구현해봤습니다.
1. 무한스크롤 리스너 클래스 추가
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
abstract class EndlessRecyclerViewScrollListener : RecyclerView.OnScrollListener {
    // The minimum amount of items to have below your current scroll position
    // before loading more.
    private var visibleThreshold = 5
    // The current offset index of data you have loaded
    private var currentPage = 0
    // The total number of items in the dataset after the last load
    private var previousTotalItemCount = 0
    // True if we are still waiting for the last set of data to load.
    private var loading = true
    // Sets the starting page index
    private val startingPageIndex = 0
    var mLayoutManager: RecyclerView.LayoutManager
    constructor(layoutManager: LinearLayoutManager) {
        mLayoutManager = layoutManager
    }
    constructor(layoutManager: GridLayoutManager) {
        mLayoutManager = layoutManager
        visibleThreshold *= layoutManager.spanCount
    }
    constructor(layoutManager: StaggeredGridLayoutManager) {
        mLayoutManager = layoutManager
        visibleThreshold *= layoutManager.spanCount
    }
    private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
        var maxSize = 0
        for (i in lastVisibleItemPositions.indices) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i]
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i]
            }
        }
        return maxSize
    }
    // This happens many times a second during a scroll, so be wary of the code you place here.
    // We are given a few useful parameters to help us work out if we need to load some more data,
    // but first we check if we are waiting for the previous load to finish.
    override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
        var lastVisibleItemPosition = 0
        val totalItemCount = mLayoutManager.itemCount
        if (mLayoutManager is StaggeredGridLayoutManager) {
            val lastVisibleItemPositions =
                (mLayoutManager as StaggeredGridLayoutManager).findLastVisibleItemPositions(null)
            // get maximum element within the list
            lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
        } else if (mLayoutManager is GridLayoutManager) {
            lastVisibleItemPosition =
                (mLayoutManager as GridLayoutManager).findLastVisibleItemPosition()
        } else if (mLayoutManager is LinearLayoutManager) {
            lastVisibleItemPosition =
                (mLayoutManager as LinearLayoutManager).findLastVisibleItemPosition()
        }
        // If the total item count is zero and the previous isn't, assume the
        // list is invalidated and should be reset back to initial state
        if (totalItemCount < previousTotalItemCount) {
            currentPage = startingPageIndex
            previousTotalItemCount = totalItemCount
            if (totalItemCount == 0) {
                loading = true
            }
        }
        // If it’s still loading, we check to see if the dataset count has
        // changed, if so we conclude it has finished loading and update the current page
        // number and total item count.
        if (loading && totalItemCount > previousTotalItemCount) {
            loading = false
            previousTotalItemCount = totalItemCount
        }
        // If it isn’t currently loading, we check to see if we have breached
        // the visibleThreshold and need to reload more data.
        // If we do need to reload some more data, we execute onLoadMore to fetch the data.
        // threshold should reflect how many total columns there are too
        if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
            currentPage++
            onLoadMore(currentPage, totalItemCount, view)
            loading = true
        }
    }
    // Call this method whenever performing new searches
    fun resetState() {
        currentPage = startingPageIndex
        previousTotalItemCount = 0
        loading = true
    }
    // Defines the process for actually loading more data based on page
    abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)
}
2. 어댑터(어댑터에는 따로 해줄건 없습니다.)
package com.mtjin.androidarchitecturestudy.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RatingBar
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.Movie
class MovieAdapter :
    RecyclerView.Adapter<MovieAdapter.ViewHolder>() {
    private lateinit var clickCallBack: (Movie) -> Unit
    private val items: ArrayList<Movie> = ArrayList()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view: View = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_movie, parent, false)
        val viewHolder = ViewHolder(view)
        view.setOnClickListener {
            clickCallBack(items[viewHolder.adapterPosition])
        }
        return viewHolder
    }
    override fun getItemCount(): Int = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        items[position].let {
            holder.bind(it)
        }
    }
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val ivPoster = itemView.findViewById<ImageView>(R.id.iv_poster)
        private val rvRating = itemView.findViewById<RatingBar>(R.id.rb_rating)
        private val tvTitle = itemView.findViewById<TextView>(R.id.tv_title)
        private val tvReleaseDate = itemView.findViewById<TextView>(R.id.tv_release_date)
        private val tvActor = itemView.findViewById<TextView>(R.id.tv_actor)
        private val tvDirector = itemView.findViewById<TextView>(R.id.tv_director)
        fun bind(movie: Movie) {
            with(movie) {
                Glide.with(itemView).load(image)
                    .placeholder(R.drawable.ic_default)
                    .into(ivPoster!!)
                rvRating.rating = (userRating.toFloatOrNull() ?: 0f) / 2
                tvTitle.text = HtmlCompat.fromHtml(title, HtmlCompat.FROM_HTML_MODE_COMPACT)
                tvReleaseDate.text = pubDate
                tvActor.text = actor
                tvDirector.text = director
            }
        }
    }
    fun setItems(items: List<Movie>) {
        this.items.addAll(items)
        notifyDataSetChanged()
    }
    fun setItemClickListener(clickCallBack: (Movie) -> Unit) {
        this.clickCallBack = clickCallBack
    }
    fun clear() {
        this.items.clear()
        notifyDataSetChanged()
    }
}
3. 액티비티
저는 서버에 요청할 때 page가 아닌 아이템의 개수가 필요해서 totalItemsCount를 사용했습니다. 스크롤 조건이 된 경우 requestPagingMovie를 호출해줍니다.
private fun initAdapter() {
        movieAdapter = MovieAdapter()
        val linearLayoutManager = LinearLayoutManager(this)
        rvMovies.layoutManager = linearLayoutManager
        scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                requestPagingMovie(query, totalItemsCount + 1)
            }
        }
        rvMovies.addOnScrollListener(scrollListener)
        rvMovies.adapter = movieAdapter
    }
만약 새로 영화 검색을 해야하는 등 무한스크롤을 초기화 시켜야하는 경우는
scrooListener.resetState()를 해주면 됩니다.
private fun requestMovie(query: String) {
        showLoading()
        scrollListener.resetState()
        myApplication.movieRepository.getSearchMovies(query,
            success = {
                if (it.isEmpty()) {
                    onToastMessage("해당 영화는 존재하지 않습니다.")
                } else {
                    movieAdapter.clear()
                    movieAdapter.setItems(it)
                    onToastMessage("영화를 불러왔습니다.")
                }
                hideLoading()
            },
            fail = {
                Log.d(TAG, it.toString())
                when (it) {
                    is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
                    else -> onToastMessage(it.message.toString())
                }
                hideLoading()
            })
    }
영화를 더 불러오는 함수입니다.
fun requestPagingMovie(query: String, offset: Int) {
        showLoading()
        myApplication.movieRepository.getPagingMovies(query, offset,
            success = {
                if (it.isEmpty()) {
                    onToastMessage("마지막 페이지")
                } else {
                    movieAdapter.setItems(it)
                    onToastMessage("영화를 불러왔습니다.")
                }
                hideLoading()
            },
            fail = {
                Log.d(TAG, it.toString())
                when (it) {
                    is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
                    else -> onToastMessage(it.message.toString())
                }
                hideLoading()
            })
    }
전체 액티비티 코드입니다.
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.utils.MyApplication
import retrofit2.HttpException
class MovieSearchActivity : AppCompatActivity() {
    private lateinit var etInput: EditText
    private lateinit var btnSearch: Button
    private lateinit var rvMovies: RecyclerView
    private lateinit var pbLoading: ProgressBar
    private lateinit var movieAdapter: MovieAdapter
    private lateinit var myApplication: MyApplication
    private lateinit var query: String
    private lateinit var scrollListener: EndlessRecyclerViewScrollListener
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movie_search)
        initView()
        initAdapter()
        initListener()
    }
    private fun initView() {
        myApplication = application as MyApplication
        etInput = findViewById(R.id.et_input)
        btnSearch = findViewById(R.id.btn_search)
        rvMovies = findViewById(R.id.rv_movies)
        pbLoading = findViewById(R.id.pb_loading)
    }
    private fun initAdapter() {
        movieAdapter = MovieAdapter()
        val linearLayoutManager = LinearLayoutManager(this)
        rvMovies.layoutManager = linearLayoutManager
        scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                requestPagingMovie(query, totalItemsCount + 1)
            }
        }
        rvMovies.addOnScrollListener(scrollListener)
        rvMovies.adapter = movieAdapter
    }
    private fun initListener() {
        //어댑터 아이템 클릭리스너
        movieAdapter.setItemClickListener { movie ->
            Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
                it.resolveActivity(packageManager) != null
            }?.run(this::startActivity)
        }
        //검색버튼
        btnSearch.setOnClickListener {
            query = etInput.text.toString().trim()
            if (query.isEmpty()) {
                onToastMessage("검색어를 입력해주세요.")
            } else {
                onToastMessage("잠시만 기다려주세요.")
                requestMovie(query)
            }
        }
    }
    private fun requestMovie(query: String) {
        showLoading()
        scrollListener.resetState()
        myApplication.movieRepository.getSearchMovies(query,
            success = {
                if (it.isEmpty()) {
                    onToastMessage("해당 영화는 존재하지 않습니다.")
                } else {
                    movieAdapter.clear()
                    movieAdapter.setItems(it)
                    onToastMessage("영화를 불러왔습니다.")
                }
                hideLoading()
            },
            fail = {
                Log.d(TAG, it.toString())
                when (it) {
                    is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
                    else -> onToastMessage(it.message.toString())
                }
                hideLoading()
            })
    }
    fun requestPagingMovie(query: String, offset: Int) {
        showLoading()
        myApplication.movieRepository.getPagingMovies(query, offset,
            success = {
                if (it.isEmpty()) {
                    onToastMessage("마지막 페이지")
                } else {
                    movieAdapter.setItems(it)
                    onToastMessage("영화를 불러왔습니다.")
                }
                hideLoading()
            },
            fail = {
                Log.d(TAG, it.toString())
                when (it) {
                    is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
                    else -> onToastMessage(it.message.toString())
                }
                hideLoading()
            })
    }
    private fun onToastMessage(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
    private fun showLoading() {
        pbLoading.visibility = View.VISIBLE
    }
    private fun hideLoading() {
        pbLoading.visibility = View.GONE
    }
    companion object {
        const val TAG = "MovieSearchActivity"
    }
}
참고 및 출처 :
https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView
codepath/android_guides
Extensive Open-Source Guides for Android Developers - codepath/android_guides
github.com
댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!
728x90
    
    
  '안드로이드 > 코틀린 & 아키텍처 & Recent' 카테고리의 다른 글
			  Comments
			                 
                 
  
  
		
	
               
           
					
					
					
					
					
					
				 
								 
								 
								