안드로이드/개발 관련

[Kotlin] 안드로이드 리사이클러 뷰 새로고침 기능 사용해보기. SwipeRefreshLayout 사용.

kimyunseok 2021. 10. 14. 16:23

가끔 앱들을 보면, 화면을 아래로 당기면 리스트가 새로고침이 되는 어플들이 있을 것이다.

이런 기능은 어떻게 구현한 것인지 항상 궁금했었는데, 오늘 알아보고 한 번 정리해 보겠다.

 

 

Swiperefreshlayout  |  Android 개발자  |  Android Developers

Swiperefreshlayout 스와이프하여 새로고침 UI 패턴을 구현합니다. 최근 업데이트 현재 안정화 버전 다음 버전 후보 베타 버전 알파 버전 2020년 7월 22일 1.1.0 - - 1.2.0-alpha01 종속 항목 선언 SwipeRefreshLayout

developer.android.com

오늘 이용할 라이브러리는 이 Swiperefreshlayout이라는 라이브러리다.

안드로이드 Jetpack의 구성요소이며, 화면을 아래로 당겼을 때, 새로고침 되도록 하는게 주요 기능이다.

오늘 처음 사용해봤는데 어렵지 않게 사용할 수 있었다.

 

코드 살펴보기

App 수준의 Build.gradle에 swiperefreshlayout에 대한 의존성을 추가한다.

최신 버전은 위에 올린 공식 문서에서 확인 가능하다 !

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    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:id="@+id/swipe_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    app:layout_constraintTop_toBottomOf="@id/textView">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="위로 스크롤 시 새로고침"
                android:textColor="@color/black"
                android:textSize="21sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/refresh_recyclerView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@id/textView"/>

            <View
                android:id="@+id/divider"
                android:layout_width="match_parent"
                android:layout_height="2dp"
                android:layout_marginTop="20dp"
                android:layout_marginHorizontal="6dp"
                android:background="@color/black"
                app:layout_constraintTop_toBottomOf="@+id/refresh_recyclerView"  />

            <TextView
                android:id="@+id/textView2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:text="무한 스크롤"
                android:textColor="@color/black"
                android:textSize="21sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/divider" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/infinity_recyclerView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                app:layout_constraintTop_toBottomOf="@+id/textView2" />


        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

메인 액티비티 레이아웃 xml파일.

메인 액티비티 레이아웃 모습

아래 무한 스크롤 부분 리사이클러뷰는 무시해도 된다.

이 다음 글에 페이징 3를 사용한 무한 스크롤 기능을 공부해볼 예정이라서 만들어 논 것이다.

 

간단하게, 최상위 레이아웃이 오늘 사용해볼 SwipeRefreshLayout이다.

그리고 그 안에 NestedScrollView가 존재한다.

처음에 이 둘의 순서를 바꿔서 UI를 만들었는데, 순서를 바꿀 경우, SwipeRefreshLayout가 사라지는 문제가발생했다.원래는 SwipeRefreshLayout만 스크롤해서 그 안에 있는 것을 새로고침 하도록 만들고 싶었는데, 애초에그런 기능이 아니라 화면 자체를 위로 스크롤해야 새로고침이 되는 것 같았다.(확실한 것은 아니지만, 생각해보면 대체로 앱에서 로딩하는 게 최상단에 있는 것을 생각해보면 맞는 것 같다.)

 

화면 구성은 스와이프레이아웃 ->스크롤뷰 -> 제약 레이아웃 -> 텍스트 뷰, 리사이클러뷰(부모 -> 감싸는 자식)의 형태로 되어있다.

 

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable
            name="model"
            type="com.khs.recyclerviewrefreshandinfinityscrollexample.model.RecyclerModel" />
    </data>

    <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">

        <TextView
            android:id="@+id/emoji_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{model.emoji}"
            android:textSize="45sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/title_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{model.title}"
            android:textSize="27sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintStart_toEndOf="@+id/emoji_tv"
            app:layout_constraintBottom_toBottomOf="@+id/emoji_tv"
            app:layout_constraintTop_toTopOf="@+id/emoji_tv" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

리사이클러뷰 홀더 레이아웃 파일

[이모지] [이름]의 형태로 만들어놨다.

데이터 바인딩을 사용해서 구현했다.

뷰모델 클래스는 위처럼 생겼다.

 

package com.khs.recyclerviewrefreshandinfinityscrollexample.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.khs.recyclerviewrefreshandinfinityscrollexample.databinding.HolderRecyclerviewBinding
import com.khs.recyclerviewrefreshandinfinityscrollexample.model.RecyclerModel

class MyAdapter(context: Context, val itemList: MutableList<RecyclerModel>): RecyclerView.Adapter<MyAdapter.Holder>() {

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

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val item = itemList[position]
        holder.bind(item)
    }

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

    inner class Holder(val binding: HolderRecyclerviewBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(item: RecyclerModel) {
            binding.model = item
        }
    }

}

리사이클러뷰 어댑터 소스 파일.

뷰 바인딩과 데이터 바인딩을 이용해서 구현했다.

뷰 바인딩과 데이터 바인딩을 이용한 리사이클러뷰는 지난 글에 다뤘으니 정리는 PASS하겠다.

 

package com.khs.recyclerviewrefreshandinfinityscrollexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.khs.recyclerviewrefreshandinfinityscrollexample.adapter.MyAdapter
import com.khs.recyclerviewrefreshandinfinityscrollexample.databinding.ActivityMainBinding
import com.khs.recyclerviewrefreshandinfinityscrollexample.model.RecyclerModel
import java.util.*

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    lateinit var totalItemList: MutableList<RecyclerModel> // 모든 item을 가지고 있는 리스트

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initList()
        setUpRecyclerView()
        setUpSwipeRefresh()
    }

    // 리스트 초기화 메서드
    private fun initList() {
        totalItemList = mutableListOf(
            RecyclerModel("🌭", "핫도그"),
            RecyclerModel("🍔", "햄버거"),
            RecyclerModel("🍟", "감자튀김"),
            RecyclerModel( "🍕", "피자"),
            RecyclerModel("🍜", "라면"),
            RecyclerModel( "🍩", "도넛"),
            RecyclerModel( "🍗", "치킨")
        )
    }

    //리사이클러뷰 셋팅 메서드
    private fun setUpRecyclerView() {
        val infinityItemList = mutableListOf<RecyclerModel>() // 무한 스크롤 리사이클러뷰 아이템리스트
        setListRandom(infinityItemList)
        binding.infinityRecyclerView.adapter = MyAdapter(applicationContext, infinityItemList)
        binding.infinityRecyclerView.layoutManager = LinearLayoutManager(applicationContext)

        val refreshItemList = mutableListOf<RecyclerModel>() // 새로고침 리사이클러뷰 아이템 리스트
        setListRandom(refreshItemList)
        binding.refreshRecyclerView.adapter = MyAdapter(applicationContext, refreshItemList)
        binding.refreshRecyclerView.layoutManager = LinearLayoutManager(applicationContext)
    }

    // 스와이프 이벤트 생성 메서드
    private fun setUpSwipeRefresh() {
        //새로고침 리사이클러뷰의 어댑터를 통해 불러온 List와 원래 refreshItemList이 다른 주소를 가지고 있었음.
        binding.swipeLayout.setOnRefreshListener {
            val list = (binding.refreshRecyclerView.adapter as MyAdapter).itemList
            setListRandom(list)
            binding.swipeLayout.isRefreshing = false
            binding.refreshRecyclerView.adapter?.notifyDataSetChanged()
        }
    }

    // 새로고침 메서드
    private fun setListRandom(list: MutableList<RecyclerModel>) {
        list.clear()
        for(idx in 0..6) {
            val randomIdx = (Math.random() * 7).toInt()
            list.add(totalItemList[randomIdx])
        }
    }
}

메인 액티비티 소스 파일.

 

뷰 바인딩을 사용해서 레이아웃 뷰를 인플레이션했다.

initList()메서드로 totalItemList라는 MutableList에 값들을 넣어서 초기화해준다.

그러고 나서 리사이클러뷰를 초기화 해준다. 무한 스크롤 리사이클러뷰 초기화 부분의 설명은 생략하고

새로고침 리사이클러뷰 초기화 부분을 살펴보자면,

refreshItemList라는 MutableList를 생성한 후에, setListRandom() 메서드를 호출해서 해당 리스트에 totalItemList에 들어있는 것들 중에 랜덤한 7개의 값들을 넣어준다.

그리고 리사이클러뷰의 어댑터를 설정해주고 layoutManager를 바꿔준다.

 

setUpSwipeRefresh()메서드를 살펴보자. 이 메서드에서는 swipeLayout의 setOnRefreshListener를 설정해준다.

refreshRecyclerView의 아이템 리스트를 가져온 후에, setListRandom()메서드를 호출해서 해당 아이템 리스트를 초기화 한 후에, 7개의 아이템을 랜덤으로 다시 넣는다.

이 때 isRefreshing = false 처리를 해주지 않으면 새로고침이 종료되지 않는다.

따라서 반드시 해당 코드를 만들어줘야 새로고침이 종료된다.

새로고침이 끝나면 notifyDataSetChanged()를 호출해서 리사이클러뷰에 데이터가 변경된 것을 알려준다.(뷰 갱신용도)

 

 

우리가 사용한 setOnRefreshListener 메서드의 내부는 어떻게 생겼을까?

SwipeRefreshLayout.java 파일을 살펴보았다.

해당 메서드는 OnRefreshListener를 매개변수로 받아서 전역변수인 mListener를 바꿔준다.

그리고 mListener는 onAnimationEnd가 됐을 때, null이 아니라면 onRefresh() 메서드를 호출한다.

 

OnRefreshListener라는 인터페이스가 존재하고 내부에는 onRefresh() 메서드가 존재한다.

즉, 나는 OnRefreshListener라는 인터페이스를 구현해서 onRefresh()라는 메서드를 구현해서 쓴 것이다.

 

사용하는 것과 이해하는데에 큰 어려움이 없다.

다만 리사이클러뷰 어댑터에서 notifyDataSetChanged() 메서드 사용을 지양하라는 것 같았다.

adapter에서 해당 기능을 구현해서 ViewBinding의 invalidateAll() 기능을 사용하는 것도 고려해 볼만할 것 같다.

(물론 실제로 해보지는 않았다.)

 

다음 번에는 이 코드에 이어서 페이징3 기법을 사용해서 무한 스크롤을 구현하는 방법을 정리해 보겠다.

 

 

GitHub - kimyunseok/android-study

Contribute to kimyunseok/android-study development by creating an account on GitHub.

github.com