Nuke Olaf - Log Store
[Android] 안드로이드 리사이클러뷰 무한 스크롤 - RecyclerView OnScrollListener 본문
https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.OnScrollListener
리사이클러뷰에 무한 스크롤 기능 (페이징 로딩 처리) 을 구현하려고 한다.
리사이클러뷰에서 보여줄 데이터가 로컬에 저장되어있는 것이 아니라 서버에서 가져오는 것이기 때문에, 클라이언트 측에서는 서버로부터 정해진 양의 데이터만 요청하고, 사용자가 그 이상의 데이터를 보려고 할 경우, 그만큼을 서버에 또 더 요청에서 가져와 보여주는 형식으로 진행되어야 한다.
그래서, 사용자가 서버에서 받아온 데이터 이상의 데이터를 보려고 하는 상황에 Listener 를 달아서 데이터를 더 로드해주는 것이다. 여기에 사용하는 것이 onScrollListener 이다.
서버에서 데이터를 가져와 리사이클러뷰로 보여줘야 하는 경우,
많은 데이터를 전부다 한번에 가져오는 것은 무리가 있다.
그래서 사용자가 리사이클러뷰 스크롤을 하여 아이템을 보여지게 하는 데 필요한 경우에
무한 스크롤 기능을 구현하여 데이터를 가져올 수 있다.
무한 스크롤링도 어찌 보면 페이징 기능의 한 방식이다.
무한 스크롤 구현방법
0. 레트로핏 인터페이스 작성
레트로핏으로 서버와 통신하여 데이터를 가져올 것이다.
사용할 무한 스크롤 API 는
request 로 페이지와 아이템 개수를 요청하면,
response 로 전체 게시글 총 개수, 다음 페이지 유무, 아이템 리스트를 반환한다.
@GET("/posts")
fun getPosts(
@Query("page") page: Int,
@Query("limit") limit: Int
): Call<Posts>
data class Posts(
val total_count: Int,
val is_Next: Boolean,
val posts: List<Post>
)
1. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/infiniteRv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
2. MainActivity.kt
리사이클러뷰에 addOnScrollListener 를 달아준다
class MainActivity : AppCompatActivity() {
private var totalCount = 0 // 전체 아이템 개수
private var isNext = false // 다음 페이지 유무
private var page = 0 // 현재 페이지
private var limit = 10 // 한 번에 가져올 아이템 수
private lateinit var adapter: PostAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
adapter = PostAdapter()
infiniteRv.adapter = adapter
loadPosts()
initScrollListener()
}
// 리사이클러뷰에 최초로 넣어줄 데이터를 load 한다
private fun loadPosts() {
retrofitClient.getPosts(getPage(), limit)
.enqueue(object : Callback<Posts> {
override fun onResponse(call: Call<Posts>, response: Response<Posts>) {
val body = response.body()
if (body != null && response.isSuccessful) {
totalCount = body.total_count
isNext = body.is_Next
adapter.setPosts(body.posts)
} else {
// 통신 에러
}
}
override fun onFailure(call: Call<Posts>, t: Throwable) {
// 통신 에러
}
})
}
// 리사이클러뷰에 더 보여줄 데이터를 로드하는 경우
private fun loadMorePosts() {
adapter.setLoadingView(true)
// 너무 빨리 데이터가 로드되면 스크롤 되는 Ui 를 확인하기 어려우므로,
// Handler 를 사용하여 1초간 postDelayed 시켰다
val handler = android.os.Handler()
handler.postDelayed({
retrofitClient.getPosts(getPage(), limit)
.enqueue(object : Callback<Posts> {
override fun onResponse(call: Call<Posts>, response: Response<Posts>) {
val body = response.body()
if (body != null && response.isSuccessful) {
totalCount = body.total_count
isNext = body.is_Next
adapter.run {
setLoadingView(false)
addPosts(body.posts)
}
} else {
// 통신 에러
}
}
override fun onFailure(call: Call<Posts>, t: Throwable) {
// 통신 에러
}
})
}, 1000)
}
private fun initScrollListener() {
infiniteRv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = infiniteRv.layoutManager
// hasNextPage() -> 다음 페이지가 있는 경우
if (hasNextPage()) {
val lastVisibleItem = (layoutManager as LinearLayoutManager)
.findLastCompletelyVisibleItemPosition()
// 마지막으로 보여진 아이템 position 이
// 전체 아이템 개수보다 5개 모자란 경우, 데이터를 loadMore 한다
if (layoutManager.itemCount <= lastVisibleItem + 5) {
loadMorePosts()
setHasNextPage(false)
}
}
}
})
}
private fun getPage(): Int {
page++
return page
}
private fun hasNextPage(): Boolean {
return isNext
}
private fun setHasNextPage(b: Boolean) {
isNext = b
}
}
3. PostAdpater.kt
PostViewHolder 와 LoadingViewHolder 는 생략
adapter 의 setLoadingView(true) 함수를 호출할 경우, posts 리스트의 맨 마지막에 null 값을 추가하게 된다.
그래서 posts[position] 값이 null 인경우, ViewType 이 loadingView 가 된다.
그래서 loadMore() 함수를 호출할때, setLoadingView(true) 로 마지막 아이템을 로딩뷰로 바꿨다가,
아이템 load 가 끝나면 setLoadingView(false) 로 마지막 null 값 아이템을 제거한다
PostAdapter() : RecyclerView.Adapter<RecyclerView.ViewHolder> {
companion object {
private const val TYPE_POST = 0
private const val TYPE_LOADING = 1
}
private val posts = mutableListOf<Post?>()
fun setPosts(posts: List<Post>) {
this.posts.apply {
clear()
addAll(posts)
}
notifyDataSetChanged()
}
fun addPosts(posts: List<Post>) {
this.posts.addAll(posts)
notifyDataSetChanged()
}
fun setLoadingView(b: Boolean) {
if (b) {
android.os.Handler().post {
this.posts.add(null)
notifyItemInserted(comments.size - 1)
}
} else {
if (this.comments[comments.size - 1] == null) {
this.comments.removeAt(comments.size - 1)
notifyItemRemoved(comments.size)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
TYPE_POST -> {
val inflatedView = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_post, parent, false)
return PostViewHolder(inflatedView)
}
else -> {
val inflatedView = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_loading, parent, false)
return LoadingViewHolder(inflatedView)
}
}
}
override fun getItemCount(): Int {
return posts.size
}
override fun getItemViewType(position: Int): Int {
return when (posts[position]) {
null -> TYPE_POST
else -> TYPE_LOADING
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
TYPE_POST -> {
val postViewHolder = holder as PostViewHolder
postViewHolder.bind(posts[position])
}
}
}
}
참고 사이트 >>>
https://salix97.tistory.com/250
https://medium.com/@etiennelawlor/pagination-with-recyclerview-1cb7e66a502b
https://medium.com/@c004112/android-pagination-in-recyclerview-780043f04c21
'Android' 카테고리의 다른 글
[Android] 카카오 로그인 API 사용하기 (feat. Kotlin) (0) | 2020.03.26 |
---|---|
[Android] 당겨서 새로고침 - swipe refresh layout (0) | 2020.03.26 |
[Android] Android Keystore 보안 시스템 (0) | 2020.03.19 |
레트로핏 (Retrofit) 이란? (Kotlin 으로 레트로핏 사용) (0) | 2020.03.07 |
안드로이드 앱 아키텍처 가이드 (1) | 2020.03.07 |