본문으로 바로가기

retrofit2 + okhttp3 + recyclerview + infinite scroll

category Mobile/Android 2021. 1. 5. 17:51
반응형

안드로이드를 공부해보고자 무작정 kotlin 으로 DB 데이터 리스트 출력부터 해보았다.
(여기저기 구글링하여 많은 블로그들의 종합적인 소스를 기반으로 했기때문에 틀린 소스일 수 있음)


OkHttp

효율적으로 데이터를 로드하고 대역폭을 절약할 수 있게한다.

  • OkHttp는 기본적으로 효율적인 HTTP 클라이언트이며 HTTP/2 지원을 통해 동일한 호스트에 대한 모든 요청이
    소켓을 공유할 수 있다.
  • 연결 풀링은 요청 지연 시간을 줄인다. (HTTP/2를 사용할 수 없는 경우)
  • 투명 GZIP는 다운로드 크기를 줄인다.
  • 응답 캐시는 반복 요청에 대한 네트워크를 완전히 방지한다.

라고 OkHttp 공식 홈페이지를 번역기로 돌렸다...

그리고 Android 5.0+(API 레벨 21+) 및 Java 8+에서 작동한다.

OkHttp 공식 홈페이지


Retrofit

Retrofit은 OkHttp에 의존하며 A type-safe HTTP client for Android and Java 라고 Retrofit 공식 홈페이지에서 말하듯이 네트워크 통신을 하여 데이터를 필요한 형태로 안전하게 받을 수 있다는 의미


Recyclerview

Recyclerview는 ListView에서 좀더 유연해진 버전이라고 한다.
구글링 도중 완벽하게 설명되어있는 것 같은 블로그를 찾아서 자세한 설명은 참고하길 바란다. ListView vs Recyclerview

짧고 굵게 설명하자면 리스트를 만드는데 재사용이 좋아진 부분, LayoutManager와 ViewHolder 패턴의 의무적인 사용, Item에 대한 뷰의 변형이나 애니메이션할 수 있는 개념이 추가

(아래 이미지는 위 블로그에서 사용된 이미지)


app

app에 retrofit2, okhttp3, recyclerview 를 추가한다.

implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc03"
implementation 'com.squareup.okhttp3:okhttp:3.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-gson:2.7.2'

Retrofit은 결과를 원하는 형태로 받기 위해 converter-gson을 같이 추가한다


Recyclerview Layout

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintVertical_bias="0"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Recyclerview를 표현할 Layout을 생성한다.


Item Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/rowNo"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/rowName"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/rowSection"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/rowRegDate"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"/>

</LinearLayout>

리스트에서 row 마다 표시할 Item Layout을 생성한다. 여기서는 Text만 표시하기로함


Pojo Class (Java Beans)

test 용으로 php로 DB에서 데이터를 json으로 출력한 것을 가져오기 위한 Gson 형태의 Pojo Class

class Items {
    @SerializedName("No")
    @Expose
    private var no: String? = null

    @SerializedName("Name")
    @Expose
    private var name: String? = null

    @SerializedName("Section")
    @Expose
    private var section: String? = null

    @SerializedName("RegDate")
    @Expose
    private var regDate: String? = null

    fun getNo(): String? {
        return no
    }

    fun setNo(no: String?) {
        this.no = no
    }

    fun getName(): String? {
        return name
    }

    fun setName(name: String?) {
        this.name = name
    }

    fun getSection(): String? {
        return section
    }

    fun setSection(section: String?) {
        this.section = section
    }

    fun getRegDate(): String? {
        return regDate
    }

    fun setRegDate(regDate: String?) {
        this.regDate = regDate
    }

    override fun toString(): String {
        return "ClassPojo [RegDate = " + regDate + ", Name = " + name + ", Section = " + section + ", No = " + no + "]"
    }
}

Pojo Class 2

구조는 다음과 같다

{
    curPage : 1,
    result : [
      {
        no: 1,
        name: 2,
        section: 3,
        regDate: 4
      },
      {
      },

      ...

      n,
    ]
}
class ItemsList {
    @SerializedName("curPage")
    @Expose
    private var curPage: String? = null

    @SerializedName("result")
    @Expose
    private var items: List<Items?>? = null

    fun getCurPage(): String? {
        return curPage
    }

    fun setCurPage(curPage: String?) {
        this.curPage = curPage
    }

    fun getResult(): List<Items?>? {
        return items
    }

    fun setResult(items: List<Items?>?) {
        this.items = items
    }

    override fun toString(): String {
        return "ClassPojo [result = $items, curPage = $curPage]"
    }

}

RetrofitService

API 통신할 interface 정의

interface RetrofitService {
    @GET("/v1/test")
    fun getList(
        @Query("curPage") curPage : Int,
        @Query("setRow") setRow: Int,
        @Query("setLimit") setLimit: Int
    ): Call<ItemsList>
}

MainActivity

class MainActivity : AppCompatActivity() {

    # retrofit
    private lateinit var retrofit : Retrofit
    # retrofit service
    private lateinit var myAPI : RetrofitService
    # adapter
    private lateinit var adapter: MainAdapter
    # api query
    private lateinit var query: HashMap<String, Int>
    # infinite scroll listener
    private lateinit var scrollListener: EndlessRecyclerViewScrollListener
    # LinearLayoutManager
    private val linearLayoutManager = LinearLayoutManager(this)

    # TAG
    companion object {
        const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "on create!")
        setContentView(R.layout.activity_main)

        adapter = MainAdapter() # adapter 객체 생성
        requestMain() # 최초 데이터 호출

        recyclerView.adapter = adapter # adapter 등록
        recyclerView.layoutManager = linearLayoutManager # layoutManager 등록
    }

    # retrofit api 호출
    private fun listener(query: HashMap<String, Int>) {
        retrofit = RetrofitClient.getInstnace()
        myAPI = retrofit.create(RetrofitService::class.java)

        var result : ItemsList = ItemsList() # response 결과를 받을 변수 선언

        Runnable {
            # retrofitService interface에서 호출
            myAPI.getList(
                query.get("curPage")!!,
                query.get("setRow")!!,
                query.get("setLimit")!!
            ).enqueue(object : Callback<ItemsList> {
                override fun onFailure(call: Call<ItemsList>, t: Throwable) {
                    # 호출 실패 시
                    Log.d(TAG, t.message)
                }

                override fun onResponse(call: Call<ItemsList>, response: Response<ItemsList>) {
                    # 호출 성공 시
                    Log.d(TAG, "response : ${response.body()!!.toString()}")
                    result = response.body()!!

                    if (result.getResult()!!.isNotEmpty()) {
                        # 결과값 세팅
                        adapter.setItems((result.getResult() as ArrayList<Items>?)!!)
                    } else {
                        Log.d(TAG, "errorBody : ${response.errorBody()}")
                        Log.d(TAG, "message : ${response.message()}")
                        Log.d(TAG, "status_code : ${response.code()}")
                        Log.d(TAG, "response_url : ${response.raw().request().url().url()}")
                    }
                }
            })
        }.run()
    }

    private fun requestMain() {
        # 최초 호출
        var query: HashMap<String, Int> = hashMapOf("curPage" to 1, "setRow" to 0, "setLimit" to 10)
        listener(query)

        # 스크롤 이벤트
        scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                requestPagingMain(query, totalItemsCount + 10)
            }
        }

        recyclerView.addOnScrollListener(scrollListener)
        scrollListener.resetState()
    }

    private fun requestPagingMain(query: HashMap<String, Int>, offset: Int) {
        # 추가 호출 시
        query.put("setRow", offset)
        listener(query)
    }

    # retrofit 생성
    object RetrofitClient {
        private val API_URL = "https://www.test.co.kr/"
        private var instance : Retrofit? = null
        private val gson = GsonBuilder().setLenient().create()

        fun getInstnace() : Retrofit {
            if(instance == null){
                instance = Retrofit.Builder()
                        .baseUrl(API_URL)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build()
            }
            return instance!!
        }
    }

}

Adapter

class MainAdapter(): RecyclerView.Adapter<CustomViewHolder>(){

    private var item = ArrayList<Items>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
        val layoutInflater = LayoutInflater.from(parent?.context)
        val v = layoutInflater.inflate(R.layout.item_layout, parent, false)
        return CustomViewHolder(v)
    }

    override fun getItemCount(): Int = item.size

    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        holder?.bindItems(items[position])
    }

    fun setItems(newItem : ArrayList<Items>) {
        this.item.addAll(newItem)
        # 값이 변경된 것을 알림 (이거 안하면 안됨)
        notifyDataSetChanged()
    }

}

//뷰홀더 : 안에 정의할 수 있고 밖에서 정의 할 수 있다.
class CustomViewHolder(val view : View) : RecyclerView.ViewHolder(view){

    // 받은 뷰에 아이템 바인딩
    fun bindItems(data : Items){
        view.findViewById<TextView>(R.id.rowNo).text = data.getNo()
        view.findViewById<TextView>(R.id.rowName).text = data.getName()
        view.findViewById<TextView>(R.id.rowSection).text = data.getSection()
        view.findViewById<TextView>(R.id.rowRegDate).text = data.getRegDate()
    }
}

EndlessRecyclerViewScrollListener

해당 소스는 구글링해서 얻었다.

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?)
}

흐름정리

흐름은 좀 더 공부한 후에 수정할 예정....

반응형

'Mobile > Android' 카테고리의 다른 글

Android HASHKEY 얻는 법  (0) 2019.12.21
HTTP, Async, Gson, 이미지 라이브러리 & Json Parse  (0) 2019.12.15
Dialog ProgressBar  (0) 2019.12.15