IT 프로그래밍-Android

[Android] RecyclerView 무한스크롤(endless scroll) 만들기

godsangin 2020. 1. 16. 17:21
반응형

안녕하세요. godsangin입니다.

 

혹시 페이스북, 인스타그램에서 게시물을 내리면 계속해서 새로운 게시물을 로딩하는 구성을 보신적이 있으신가요 ??

오늘은 그와 같이 특정 아이템으로 구성된 AdapterView를 endless하게 구현하는 방법에 대하여 알아보도록 하겠습니다.

 

서버에서 데이터 불러오기 예제를 참고하시면 도움이 될 것 같습니다.

https://in-idea.tistory.com/22

 

[Android] 서버에서 데이터 불러오기

안녕하세요. godsangin입니다. 오랜만에 글을 올리게 되었는데요. 최근에 GCP(Google Cloud Platform)을 사용해 보는 관계로 이런 저런 시행착오가 있어서 글을 못썼습니다...ㅠㅠ 오늘은 서버에서 데이터를 불러..

in-idea.tistory.com

이번 예제에서는 위 게시물의 Skin이라는 객체가 아닌 Product라는 객체라는 점 유의하시길 바랍니다.

 

우선 RecyclerView에 대한 Adapter가 필요하겠죠 ??

 

package com.example.hwahae

import android.content.Context
import android.content.Intent
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat

class ProductAdapter():RecyclerView.Adapter<RecyclerView.ViewHolder>(){
    constructor(context:Context, products:List<Product>, adapterClickListener: AdapterClickListener, onLoadMoreListener: OnLoadMoreListener, gridLayoutManager: GridLayoutManager):this(){
        this.context = context
        this.products = ArrayList(products)
        this.adapterClickListener = adapterClickListener
        this.onLoadMoreListener = onLoadMoreListener
        this.gridLayoutManager = gridLayoutManager
    }
    lateinit var context:Context
    lateinit var products:ArrayList<Product?>
    lateinit var adapterClickListener:AdapterClickListener
    lateinit var onLoadMoreListener:OnLoadMoreListener
    lateinit var gridLayoutManager: GridLayoutManager

    var isModeLoading = false
    var visibleThreshold = 1
    var firstVisibleItem = 0
    var visibleItemCount = 0
    var totalItemCount = 0
    var lastVisibleItem = 0

    public interface OnLoadMoreListener{
        fun onLoadMore()
    }

    public fun setRecyclerView(view:RecyclerView){
        view.addOnScrollListener(object: RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                visibleItemCount = recyclerView.childCount
                totalItemCount = gridLayoutManager.itemCount
                firstVisibleItem = gridLayoutManager.findFirstVisibleItemPosition()
                lastVisibleItem = gridLayoutManager.findLastVisibleItemPosition()

                if(!isModeLoading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)){
                    if(onLoadMoreListener != null){
                        onLoadMoreListener.onLoadMore()
                    }
                    isModeLoading = true
                }
            }
        })
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        var view = LayoutInflater.from(context).inflate(R.layout.product_item, parent, false)
        return Holder(view)
    }


    fun addItemMore(newOne:List<Product>){
        products.addAll(newOne)
        notifyItemRangeChanged(0, products.size)
    }

    fun setMoreLoading(isModeLoading:Boolean){
        this.isModeLoading = isModeLoading
    }


    override fun getItemCount(): Int {
        return products.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as Holder).bind(products[position]!!, context)
    }

    inner class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
        //coding your own view
        val productTitle = itemView?.findViewById<TextView>(R.id.product_title)
        val productPrice = itemView?.findViewById<TextView>(R.id.product_price)
        val productImage = itemView?.findViewById<ImageView>(R.id.product_image)

       fun bind(product: Product, context: Context) {
            productTitle?.text = product.title
            productPrice?.text = product.price + "원"
            Glide.with(context).load(product.thumbnailImage)
                .override(172, 172).centerCrop().into(productImage!!)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
                val drawable = context.getDrawable(R.drawable.round_background_imageview) as GradientDrawable
                productImage.background = drawable
                productImage.clipToOutline = true
            }
            else {
                //TODO("VERSION.SDK_INT < LOLLIPOP")
            }
            itemView.setOnClickListener{
                //nextPage
                adapterClickListener.setOnClickListener(product.id)
            }
        }

    }

}

코드가 생각보다 기네요..! 뒤의 Holder부분은 본인이 필요한 방식데로 꾸며주시면 되고 Adapter에서 중요한 점은 setRecyclerView에서 recyclerview에 대한 scroll listener를 작성하는 부분과 interface인 onloadmore입니다.

recyclerview에서 보이는 아이템과 해당 아이템의 인덱스를 이용하여 리스트가 스크롤 될 때 마지막 리스트까지 불러왔는지 여부를 확인하고, Activity에서 실행될 listener의 onReadMore함수를 호출합니다.(현재 화면에서 리스트의 인덱스를 알기 위해서는 LayoutManager를 Adapter가 알고 있어야합니다. -> adapter생성 시 layoutManager를 매개변수로 두는 이유)

여기서 Activity에서 작성될 onLoadMore함수를 통하여 새로운 아이템들을 리스트에 추가하고 adapter에 notify해주면 무한스크롤 이벤트를 추가할 수 있습니다.(참 쉽죠....?)

 

다음으로 RecyclerView와 Adapter를 연결할 Activity입니다.

package com.example.hwahae

import android.content.Intent
import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.util.JsonReader
import android.util.Log
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

class MainActivity : AppCompatActivity(){
    lateinit var myHeader:ConstraintLayout
    lateinit var myRecyclerView:RecyclerView
    lateinit var recyclerAdapter:ProductAdapter
    var pageNum = 1
    var products:List<Product>? = null

    val REQUESTCODE_PRODUCT_DETAIL = 1000

    val onLoadMoreListener = object:ProductAdapter.OnLoadMoreListener{
        override fun onLoadMore() {
            pageNum++
            getProductsTask()
        }
    }
    val adapterClickListener = object:AdapterClickListener{
        override fun setOnClickListener(data: Int) {
            val intent = Intent(applicationContext, ProductDetailActivity::class.java)
            intent.putExtra("data", data)
            startActivityForResult(intent, REQUESTCODE_PRODUCT_DETAIL)
            overridePendingTransition(R.anim.anim_translate_up, R.anim.anim_translate_down)
        }
    }
    val connectionListener = object: ConnectionListener{
        override fun responseEnd() {//데이터를 전부 받아왔을 경우 실행됨
            object: Thread(){
                override fun run() {
                    super.run()
                    val msg = adapterFrontTask.obtainMessage()
                    adapterFrontTask.sendMessage(msg)
                }
            }.run()
        }
    }
    val adapterFrontTask: Handler = object: Handler(){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            if(pageNum == 1){
                val gm = GridLayoutManager(applicationContext, 2)
                recyclerAdapter =
                    ProductAdapter(applicationContext, products!!, adapterClickListener, onLoadMoreListener, gm)
                myRecyclerView.layoutManager = gm
                myRecyclerView.adapter = recyclerAdapter
                recyclerAdapter.setRecyclerView(myRecyclerView)
                recyclerAdapter.notifyDataSetChanged()
            }
            else{
                recyclerAdapter.addItemMore(products!!)
                recyclerAdapter.setMoreLoading(false)

            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        myHeader = findViewById(R.id.header)
        myRecyclerView = findViewById(R.id.item_recyclerview)
        setLayoutComponents()
    }

    fun setLayoutComponents(){
        getProductsTask()
    }

    fun getProductsTask(){
        AsyncTask.execute(object: Runnable {
            override fun run() {
                var inputStream: InputStream? = null
                try{
                    val myEndpoint = URL("insert your url here/products?page=" + pageNum)
                    val myConnection = myEndpoint.openConnection() as HttpURLConnection
                    myConnection.setRequestProperty("User-Agent", "my-rest-app-v0.1")
                    myConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
                    myConnection.requestMethod = "GET"
                    myConnection.readTimeout = 20000
                    myConnection.doInput = true

                    myConnection.connect()

                    val response = myConnection.responseCode
                    Log.d("response==", response.toString())

                    if(response == 200){
                        inputStream = myConnection.inputStream
                        products = readJsonStream(inputStream)
                        if(products != null){
                            connectionListener.responseEnd()
                        }
                    }
                }catch(e: Exception){
                    e.printStackTrace()
                    Log.e("response==", "Error:" + e.message)
                }finally {
                    if(inputStream != null){
                        inputStream.close()
                    }
                }
            }
        })
    }

    fun readJsonStream(inputStream: InputStream):List<Product>{
        val reader = JsonReader(InputStreamReader(inputStream, "UTF-8"))
        var result:List<Product> = ArrayList()
        try{
            reader.beginObject()
            while(reader.hasNext()){
                var name = reader.nextName()
                if(name.equals("statusCode")){
                    reader.nextInt()
                }
                else if(name.equals("body")){
                    result = readMessagesArray(reader)
                }else if(name.equals("scanned_count")){
                    reader.nextInt()
                    //마지막 처리
                }else{
                    reader.skipValue()
                }
            }
            reader.endObject()
        }finally {
            reader.close()
        }
        return result
    }

    fun readMessagesArray(reader: JsonReader):List<Product>{
        val messages = ArrayList<Product>()
        reader.beginArray()
        while(reader.hasNext()){
            messages.add(Product(reader))
        }
        reader.endArray()
        return messages
    }
}

 

oncreate에서 최초에 getProductsTask가 한번 실행되어 Adapter와 RecyclerView를 초기화하고 그 뒤로 Adapter의 설정을 바꿔 줄 onLoadMoreListener를 등록합니다. 2번째 이후로 실행되는 getProductsTask는 pageNum에 따라 새로운 데이터를 요청하고 adapterFontTaskListener에 정의된 대로 새로 Adapter를 구상하지 않고 앞서 설정했던 addItemMore함수를 통하여 불러온 새로운 아이템을 추가합니다.

 

이로써 RecyclerView의 무한 로딩을 구현할 수 있습니다. 저와 같이 GridLayoutManager를 사용하지 않고 LinearLayoutManager를 사용하였다면 로딩아이템도 추가해 볼 수 있습니다. (Grid에서 추가하려면 복잡한 구조가 될 것 같아 잠시 미루기로 했습니다..)

 

아래는 제가 참고한 블로그의 주소입니다.(로딩 아이템이 추가된 LinearLayoutManager형태입니다.)

https://isntyet.tistory.com/114

 

(android) RecyclerView bottom endless refresh[리스트 페이징)

기존 프로젝트에서 RecyclerView를 통해 내용을 쭉 불러오고 있었는데 실험삼아 검색기준을 바꾸어 1000개정도의 아이템을 한번에 불러왔더니 역시나 로딩이 오래걸리면서 앱이 버벅거렸다. 그래서 해결책을 찾던..

isntyet.tistory.com

다음 시간에는 그 동안 서버에서 불러오는 방식을 Retrofit을 통하여 구현하는 예제로 찾아뵙겠습니다. 아직 공부를 조금 더 해봐야 겠지만 서버 요청을 하고 Json으로 파싱하는 과정을 간소화 할 수 있을 것 같습니다.

 

부족한 글 읽어주셔서 감사합니다.

 

그럼 안녕~~